Create rich Discord interactions and respond to them using Laravel. Utilizes Discord HTTP webhooks without the need of a long-running process.
You can install the package via composer:
composer require plytas/laravel-discord-interactions
You can publish the config file with:
php artisan vendor:publish --tag="discord-interactions-config"
This is the contents of the published config file:
return [
'application_id' => env('DISCORD_APPLICATION_ID'),
'public_key' => env('DISCORD_PUBLIC_KEY'),
'bot_token' => env('DISCORD_BOT_TOKEN'),
'route' => [
'path' => env('DISCORD_ROUTE', '/discord'),
'middleware' => [
'before' => [
//ThrottleRequests::class,
],
'after' => [
],
],
],
];
Create a new discord application at https://discord.com/developers/applications.
Copy the application id and public key from "General information" tab and bot token from "Bot" tab to your .env
file.
For local development you can use services such as expose or ngrok to expose your local server to the internet.
In your discord application fill in Interactions Endpoint URL
under "General information" with the url of your server followed by the path you set in the config file. By default it is /discord
.
https://your-server-url.com/discord
When clicking the "Save Changes" button, Discord will send a couple POST
request to the provided URL to verify the endpoint. One of them will intentionally contain an invalid signature. The package will automatically respond to these requests appropriately.
If the changes are saved successfully, you are good to go!
To create a new chat command, create a new class that implements the Plytas\Discord\Contracts\DiscordCommand
interface.
use Plytas\Discord\Contracts\DiscordCommand;
use Plytas\Discord\Data\DiscordInteraction;
use Plytas\Discord\Data\DiscordMessage;
use Plytas\Discord\Data\DiscordResponse;
class PingCommand implements DiscordCommand
{
public function description(): string
{
return 'Replies with pong';
}
public function handle(DiscordInteraction $interaction): DiscordResponse
{
return $interaction->respondWithMessage(
DiscordMessage::new()
->setContent('Pong!')
->ephemeral()
);
}
}
You can register commands using Plytas\Discord\DiscordCommandRegistry::setCommands
method. This can be done in the boot()
method of a service provider.
use Plytas\Discord\DiscordCommandRegistry;
DiscordCommandRegistry::setCommands([
'ping' => PingCommand::class,
// Nested commands
'user' => [
'role' => [
'add' => AddRoleCommand::class,
'remove' => RemoveRoleCommand::class,
],
],
]);
And finally call php artisan discord:register-commands
to register the commands with Discord. It's recommended to run this command in a deployment script.
Your commands will receive a DiscordInteraction
object that contains information about the interaction. You can respond to the interaction directly by returning a DiscordResponse
object.
DidcordInteraction
object contains helper methods to create responses such as respondWithMessage()
, updateMessage()
and showModal()
.
You must respond to interactions within 3 seconds. If you fail to do so, Discord will show a generic error message to the user. If you need more time to process the interaction, it is recommended to respond with a message that indicates that the command is still processing.
After processing is done, you can use \Plytas\Discord\Facades\Discord::updateInteractionMessage()
in a \Illuminate\Support\Facades\App::terminating()
callback to update the message.
use Illuminate\Support\Facades\App;
use Plytas\Discord\Contracts\DiscordCommand;
use Plytas\Discord\Data\DiscordInteraction;
use Plytas\Discord\Data\DiscordMessage;
use Plytas\Discord\Data\DiscordResponse;
use Plytas\Discord\Facades\Discord;
class PingCommand implements DiscordCommand
{
public function description(): string
{
return 'Replies with pong';
}
public function handle(DiscordInteraction $interaction): DiscordResponse
{
// Register a terminating callback to update the message after processing is done
App::terminating(function () use ($interaction) {
// Simulate a long-running process
sleep(5);
Discord::updateInteractionMessage(
interaction: $interaction,
message: DiscordMessage::new()
->setContent('Pong!')
->ephemeral()
);
});
// Respond with a message that indicates that the command is still processing
return $interaction->respondWithMessage(
DiscordMessage::new()
->setContent('Calculating...')
->ephemeral() // Only the user who invoked the command can see the message
);
}
}
You can add components to your messages to allow users to interact with them.
use Plytas\Discord\Components\ActionRow;
use Plytas\Discord\Components\Button;
use Plytas\Discord\Contracts\DiscordCommand;
use Plytas\Discord\Data\DiscordInteraction;
use Plytas\Discord\Data\DiscordMessage;
use Plytas\Discord\Data\DiscordResponse;
use Plytas\Discord\Enums\ButtonStyle;
class RockPaperScissorsCommand implements DiscordCommand
{
public function description(): string
{
return 'Play rock-paper-scissors';
}
public function handle(DiscordInteraction $interaction): DiscordResponse
{
return $interaction->respondWithMessage(
DiscordMessage::new()
->setContent('Select your move')
->addComponent(
component: ActionRow::new()
->addComponent(
component: Button::new()
->setCustomId('rock')
->setLabel('Rock')
->setEmoji('πͺ¨')
->setStyle(ButtonStyle::Primary)
)
->addComponent(
component: Button::new()
->setCustomId('paper')
->setLabel('Paper')
->setEmoji('π')
->setStyle(ButtonStyle::Primary)
)
->addComponent(
component: Button::new()
->setCustomId('scissors')
->setLabel('Scissors')
->setEmoji('βοΈ')
->setStyle(ButtonStyle::Primary)
)
)
->ephemeral() // Only the user who invoked the command can see the message
);
}
}
You can respond to component interactions by creating a new class that implements the \Plytas\Discord\Contracts\DiscordComponentHandler
interface.
use Illuminate\Support\Arr;
use Plytas\Discord\Contracts\DiscordComponentHandler;
use Plytas\Discord\Data\DiscordInteraction;
use Plytas\Discord\Data\DiscordMessage;
use Plytas\Discord\Data\DiscordResponse;
class RockPaperScissorsComponentHandler implements DiscordComponentHandler
{
public function handle(DiscordInteraction $interaction): DiscordResponse
{
$playerSelection = $interaction->getMessageComponent()->custom_id;
$botSelection = Arr::random(['rock', 'paper', 'scissors']);
if ($playerSelection === $botSelection) {
$message = 'It\'s a tie!';
}
$winningMoves = [
'rock' => 'scissors',
'paper' => 'rock',
'scissors' => 'paper',
];
if ($winningMoves[$playerSelection] === $botSelection) {
$message = 'You win!';
} else {
$message = 'You lose!';
}
return $interaction->updateMessage(
DiscordMessage::new()
->setContent($message)
->ephemeral()
);
}
}
You can register component handlers using Plytas\Discord\DiscordComponentRegistry::setComponentHandlers()
method. This method accepts an array of component_id => DiscordComponentHandler class
. This can be done in the boot()
method of a service provider.
use Plytas\Discord\DiscordComponentRegistry;
DiscordComponentRegistry::setComponentHandlers([
'rock' => RockPaperScissorsComponentHandler::class,
'paper' => RockPaperScissorsComponentHandler::class,
'scissors' => RockPaperScissorsComponentHandler::class,
]);
If you need more control over the component handler registration process, you can use Plytas\Discord\DiscordComponentRegistry::handleComponentsUsing()
method.
This method accepts a closure that receives Plytas\Discord\Data\DiscordInteraction
object and expects a Plytas\Discord\Data\DiscordResponse
object to be returned.
Imagine that your components have a dynamic custom id that contains the name of the game. You can use the following code to handle the components:
use Illuminate\Support\Str;
use Plytas\Discord\Data\DiscordInteraction;
use Plytas\Discord\Events\ComponentHandlerRegistered;
DiscordComponentRegistry::handleComponentsUsing(function(DiscordInteraction $interaction) {
$gameName = Str::afterLast($interaction->getMessageComponent()->custom_id, ':');
return match ($gameName) {
'rock-paper-scissors' => (new RockPaperScissorsComponentHandler)->handle($interaction),
'coin-flip' => (new CoinFlipComponentHandler)->handle($interaction),
'default' => $interaction->updateMessage(
DiscordMessage::new()
->setContent('Invalid game')
->ephemeral()
);
};
});
Warning
Not all components and API features are supported yet. This package started as a personal project and is still in development. If you need a specific feature, feel free to open an issue or a pull request.
composer test
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.