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

Commit 033801e

Browse files
committed
Added closer integration with DiscordPHP
- Listening for INTEGRATION_CREATE event
1 parent 500677c commit 033801e

File tree

6 files changed

+479
-35
lines changed

6 files changed

+479
-35
lines changed

README.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,20 @@ $client->deleteCommand($command);
6666

6767
### `Discord\Slash\Client`
6868

69+
There are two ways to set up the slash client:
70+
- Webhook method
71+
- Gateway method (recommended)
72+
73+
Please read both sections as both have important information and both have advantages/disadvantages.
74+
75+
#### Webhook method
76+
6977
Now that you have registered commands, you can set up an HTTP server to listen for requests from Discord.
7078

7179
There are a few ways to set up an HTTP server to listen for requests:
7280
- The built-in ReactPHP HTTP server.
73-
- Using an external HTTP server such as Apache or nginx.
7481
- Using the built-in ReactPHP HTTP server without HTTPS and using Apache or nginx as a reverse proxy (recommended).
82+
- Using an external HTTP server such as Apache or nginx.
7583

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

@@ -122,7 +130,7 @@ $client->registerCommand('hello', function (Interaction $interaction, Choices $c
122130
$client->run();
123131
```
124132

125-
This library only handles slash commands, and there is no support for any other interactions with Discord such as creating channels, sending other messages etc. You can easily combine the DiscordPHP library with this library to have a much larger collection of tools. All you must do is ensure both clients share the same ReactPHP event loop. In the future, there will be more integration between the libraries. Here is an example:
133+
This library only handles slash commands, and there is no support for any other interactions with Discord such as creating channels, sending other messages etc. You can easily combine the DiscordPHP library with this library to have a much larger collection of tools. All you must do is ensure both clients share the same ReactPHP event loop. Here is an example:
126134

127135
```php
128136
<?php
@@ -145,11 +153,20 @@ $client = new Client([
145153
'loop' => $discord->getLoop(),
146154
]);
147155

156+
$client->linkDiscord($discord, false); // false signifies that we still want to use the HTTP server - default is true, which will use gateway
157+
148158
$discord->on('ready', function (Discord $discord) {
149159
// DiscordPHP is ready
150160
});
151161

152162
$client->registerCommand('my_cool_command', function (Interaction $interaction, Choices $choices) use ($discord) {
163+
// there are a couple fields in $interaction that will return DiscordPHP parts:
164+
$interaction->guild;
165+
$interaction->channel;
166+
$interaction->member;
167+
168+
// if you don't link DiscordPHP, it will simply return raw arrays
169+
153170
$discord->guilds->get('id', 'coolguild')->members->ban(); // do something ?
154171
$interaction->acknowledge();
155172
});
@@ -212,12 +229,50 @@ $ vim /etc/apache2/sites-available/000-default.conf # default site
212229
SSLCertificateFile /path/to/ssl/cert.crt # change to your cert path
213230
SSLCertificateKeyFile /path/to/ssl/cert.key # change to your key path
214231
</VirtualHost>
215-
```
232+
```
216233
3. Restart apache - the code below works on Debian-based systems:
217234
```shell
218235
$ sudo service apache2 restart
219236
```
220237

238+
#### Gateway method (recommended)
239+
240+
The client can connect with a regular [DiscordPHP](https://github.com/discord-php/DiscordPHP) client to listen for interactions over gateway.
241+
To use this method, make sure there is no interactions endpoint set in your Discord developer application.
242+
243+
Make sure you have included DiscordPHP into your project (at the time of writing, only DiscordPHP `develop` branch is supported):
244+
245+
```sh
246+
$ composer require team-reflex/discord-php dev-develop
247+
```
248+
249+
You can then create both clients and link them:
250+
251+
```php
252+
<?php
253+
254+
include 'vendor/autoload.php';
255+
256+
use Discord\Discord;
257+
use Discord\Slash\Client;
258+
259+
$discord = new Discord([
260+
'token' => 'abcd.efdgh.asdas',
261+
]);
262+
263+
$client = new Client([
264+
'loop' => $discord->getLoop(), // Discord and Client MUST share event loops
265+
]);
266+
267+
$client->linkDiscord($discord);
268+
269+
$client->registerCommand(...);
270+
271+
$discord->run();
272+
```
273+
274+
The gateway method is much easier to set up as you do not have to worry about SSL certificates.
275+
221276
## License
222277

223278
This software is licensed under the GNU General Public License v3.0 which can be viewed in the LICENSE.md file.

composer.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
"symfony/options-resolver": "^5.2",
1616
"monolog/monolog": "^2.2",
1717
"guzzlehttp/guzzle": "^7.2",
18-
"discord/interactions": "^1.0"
18+
"discord/interactions": "^1.0",
19+
"discord-php/http": "^8.0"
1920
},
2021
"autoload": {
2122
"psr-4": {
2223
"Discord\\Slash\\": "src/Discord"
2324
}
2425
},
2526
"require-dev": {
26-
"symfony/var-dumper": "^5.2"
27+
"symfony/var-dumper": "^5.2",
28+
"team-reflex/discord-php": "dev-develop"
2729
},
2830
"suggest": {
29-
"kambo/httpmessage": "Required for hosting the command server behind a CGI/FPM server."
31+
"kambo/httpmessage": "Required for hosting the command server behind a CGI/FPM server.",
32+
"team-reflex/discord-php": "Provides an easier interface, where interactions are sent via websocket rather than a webhook."
3033
}
3134
}

src/Discord/Client.php

Lines changed: 136 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
namespace Discord\Slash;
1313

14+
use Discord\Discord;
15+
use Discord\Http\Drivers\React;
16+
use Discord\Http\Http;
1417
use Discord\Interaction as DiscordInteraction;
1518
use Discord\InteractionResponseType;
1619
use Discord\InteractionType;
1720
use Discord\Slash\Parts\Interaction;
1821
use Discord\Slash\Parts\RegisteredCommand;
22+
use Discord\WebSockets\Event;
1923
use Exception;
2024
use InvalidArgumentException;
2125
use Kambo\Http\Message\Environment\Environment;
@@ -32,6 +36,7 @@
3236
use React\Promise\Promise;
3337
use React\Socket\Server as SocketServer;
3438
use Symfony\Component\OptionsResolver\OptionsResolver;
39+
use Throwable;
3540

3641
/**
3742
* The Client class acts as an HTTP web server to handle requests from Discord when a command
@@ -72,18 +77,90 @@ class Client
7277
*/
7378
private $commands;
7479

80+
/**
81+
* ReactPHP event loop.
82+
*
83+
* @var LoopInterface
84+
*/
85+
private $loop;
86+
7587
/**
7688
* Logger for client.
7789
*
7890
* @var LoggerInterface
7991
*/
8092
private $logger;
8193

94+
/**
95+
* Optional Discord client.
96+
*
97+
* @var \Discord\Discord
98+
*/
99+
private $discord;
100+
101+
/**
102+
* Will we listen for gateway events or start HTTP server?
103+
*
104+
* @var bool
105+
*/
106+
private $interactionsOverGateway = false;
107+
108+
/**
109+
* HTTP client.
110+
*
111+
* @var Http
112+
*/
113+
private $http;
114+
82115
public function __construct(array $options = [])
83116
{
84117
$this->options = $this->resolveOptions($options);
118+
$this->loop = $this->options['loop'];
85119
$this->logger = $this->options['logger'];
86-
$this->registerServer();
120+
121+
$this->loop->futureTick(function () {
122+
if ($this->interactionsOverGateway) {
123+
$this->logger->info('not starting http server - will wait for gateway events');
124+
125+
return;
126+
}
127+
128+
$this->registerServer();
129+
});
130+
}
131+
132+
/**
133+
* Links the slash command client with a DiscordPHP client.
134+
* This will do a couple things:
135+
* - Interactions will be provided as "rich", meaning that the properties will be parts from DiscordPHP.
136+
* - If the `$interactionsOverGateway` parameter is true, the client will listen for interactions via
137+
* gateway and the HTTP server will not be started.
138+
*
139+
* @param Discord $discord
140+
* @param bool $interactionsOverGateway
141+
*/
142+
public function linkDiscord(Discord $discord, bool $interactionsOverGateway = true)
143+
{
144+
$this->discord = $discord;
145+
$this->interactionsOverGateway = $interactionsOverGateway;
146+
147+
if ($this->discord->getLoop() !== $this->loop) {
148+
throw new \RuntimeException('The Discord and slash client do not share the same event loop.');
149+
}
150+
151+
$this->http = $discord->getHttp();
152+
153+
if ($interactionsOverGateway) {
154+
$discord->on(Event::INTERACTION_CREATE, function ($interaction) {
155+
// possibly the laziest thing ive ever done - stdClass -> array
156+
$interaction = json_decode(json_encode($interaction), true);
157+
$interaction = new Interaction($interaction, $this->discord, $this->http, $this->options['application_id'] ?? null);
158+
159+
$this->handleInteraction($interaction)->done(function ($response) use ($interaction) {
160+
$this->handleGatewayInteractionResponse($response, $interaction);
161+
});
162+
});
163+
}
87164
}
88165

89166
/**
@@ -104,14 +181,16 @@ private function resolveOptions(array $options): array
104181
'loop',
105182
'public_key',
106183
'socket_options',
184+
'application_id',
185+
'token',
107186
])
108187
->setDefaults([
109188
'uri' => '0.0.0.0:80',
110189
'loop' => Factory::create(),
111190
'socket_options' => [],
112-
])
113-
->setRequired([
114-
'public_key',
191+
'public_key' => null,
192+
'application_id' => null,
193+
'token' => null,
115194
]);
116195

117196
$options = $resolver->resolve($options);
@@ -130,12 +209,41 @@ private function registerServer()
130209
{
131210
// no uri => cgi/fpm
132211
if (is_null($this->options['uri'])) {
212+
$this->logger->info('running in CGI/FPM mode - follow up messages will not work');
213+
133214
return;
134215
}
135216

136-
$this->server = new HttpServer($this->getLoop(), [$this, 'handleRequest']);
217+
$this->server = new HttpServer($this->getLoop(), function (ServerRequestInterface $request) {
218+
$identifier = sprintf('%s %s %s', $request->getMethod(), $request->getRequestTarget(), $request->getHeaderLine('User-Agent'));
219+
220+
return $this->handleRequest($request)->then(function (Response $response) use ($identifier) {
221+
$this->logger->info("{$identifier} {$response->getStatusCode()} {$response->getReasonPhrase()}");
222+
223+
return $response;
224+
}, function (Throwable $e) use ($identifier) {
225+
$this->logger->warning("{$identifier} {$e->getMessage()}");
226+
});
227+
});
137228
$this->socket = new SocketServer($this->options['uri'], $this->getLoop(), $this->options['socket_options']);
138229
$this->server->listen($this->socket);
230+
231+
// already provided HTTP client through DiscordPHP
232+
if (! is_null($this->http)) {
233+
$this->logger->info('using DiscordPHP http client');
234+
235+
return;
236+
}
237+
238+
if (! isset($this->options['token'])) {
239+
$this->logger->warning('no token provided - http client will not work');
240+
}
241+
242+
if (! isset($this->options['application_id'])) {
243+
$this->logger->warning('no application id provided - some methods may not work');
244+
}
245+
246+
$this->http = new Http('Bot '.$this->options['token'], $this->loop, $this->logger, new React($this->loop, $this->options['socket_options']));
139247
}
140248

141249
/**
@@ -145,6 +253,12 @@ private function registerServer()
145253
*/
146254
public function handleRequest(ServerRequestInterface $request)
147255
{
256+
if (! isset($this->options['public_key'])) {
257+
$this->logger->warning('A public key was not given to the slash client. Unable to validate request.');
258+
259+
return \React\Promise\Resolve(new Response(401, [0], 'Not verified'));
260+
}
261+
148262
// validate request with public key
149263
$signature = $request->getHeaderLine('X-Signature-Ed25519');
150264
$timestamp = $request->getHeaderLine('X-Signature-Timestamp');
@@ -153,7 +267,7 @@ public function handleRequest(ServerRequestInterface $request)
153267
return \React\Promise\Resolve(new Response(401, [0], 'Not verified'));
154268
}
155269

156-
$interaction = new Interaction(json_decode($request->getBody(), true));
270+
$interaction = new Interaction(json_decode($request->getBody(), true), $this->discord, $this->http, $this->options['application_id'] ?? null);
157271

158272
$this->logger->info('received interaction', $interaction->jsonSerialize());
159273

@@ -211,6 +325,18 @@ private function handleApplicationCommand(Interaction $interaction): void
211325
$checkCommand($interaction->data);
212326
}
213327

328+
/**
329+
* Handles the user response from the command when the interaction
330+
* originates from the gateway.
331+
*
332+
* @param array $response
333+
* @param Interaction $interaction
334+
*/
335+
public function handleGatewayInteractionResponse(array $response, Interaction $interaction)
336+
{
337+
$this->discord->getHttp()->post("interactions/{$interaction->id}/{$interaction->token}/callback", $response)->done();
338+
}
339+
214340
/**
215341
* Registeres a command with the client.
216342
*
@@ -230,7 +356,7 @@ public function registerCommand($name, callable $callback = null): RegisteredCom
230356
if (isset($this->commands[$name])) {
231357
throw new InvalidArgumentException("The command `{$name}` already exists.");
232358
}
233-
359+
234360
return $this->commands[$name] = new RegisteredCommand($name, $callback);
235361
}
236362

@@ -251,14 +377,14 @@ public function runCgi()
251377
if (empty($_SERVER['REMOTE_ADDR'])) {
252378
throw new Exception('The `runCgi()` method must only be called from PHP-CGI/FPM.');
253379
}
254-
380+
255381
if (! class_exists(Environment::class)) {
256382
throw new Exception('The `kambo/httpmessage` package must be installed to handle slash command interactions with a CGI/FPM server.');
257383
}
258384

259385
$environment = new Environment($_SERVER, fopen('php://input', 'w+'), $_POST, $_COOKIE, $_FILES);
260386
$serverRequest = (new ServerRequestFactory())->create($environment);
261-
387+
262388
$this->handleRequest($serverRequest)->then(function (Response $response) {
263389
http_response_code($response->getStatusCode());
264390
echo (string) $response->getBody();
@@ -280,6 +406,6 @@ public function run()
280406
*/
281407
public function getLoop(): LoopInterface
282408
{
283-
return $this->options['loop'];
409+
return $this->loop;
284410
}
285411
}

0 commit comments

Comments
 (0)