├── .github ├── coverage-badge.svg └── workflows │ ├── coverage.yml │ ├── develop-branch.yml │ ├── main-branch.yml │ ├── pull-request.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── config └── discord.php ├── phpunit.xml ├── src ├── Channels │ └── DiscordNotificationChannel.php ├── Contracts │ ├── Channels │ │ └── DiscordNotificationChannelContract.php │ ├── Listeners │ │ ├── ApplicationCommandInteractionEventListenerContract.php │ │ └── MessageComponentInteractionEventListenerContract.php │ ├── Notifications │ │ └── DiscordNotificationContract.php │ ├── Services │ │ ├── DiscordApiServiceContract.php │ │ ├── DiscordApplicationCommandServiceContract.php │ │ └── DiscordInteractionServiceContract.php │ └── Support │ │ └── Builder │ │ └── EmbedBuilderContract.php ├── Events │ ├── AbstractInteractionEvent.php │ ├── ApplicationCommandInteractionEvent.php │ └── MessageComponentInteractionEvent.php ├── Providers │ └── DiscordBotServiceProvider.php ├── Services │ ├── DiscordApiService.php │ ├── DiscordApplicationCommandService.php │ └── DiscordInteractionService.php └── Support │ ├── Builder │ ├── ComponentBuilder.php │ └── EmbedBuilder.php │ ├── Command.php │ ├── Commands │ ├── CommandOption.php │ ├── MessageCommand.php │ ├── Options │ │ ├── AttachmentOption.php │ │ ├── BooleanOption.php │ │ ├── ChannelOption.php │ │ ├── IntegerOption.php │ │ ├── MentionableOption.php │ │ ├── NumberOption.php │ │ ├── OptionChoice.php │ │ ├── RoleOption.php │ │ ├── StringOption.php │ │ ├── SubCommandGroupOption.php │ │ ├── SubCommandOption.php │ │ └── UserOption.php │ ├── SlashCommand.php │ └── UserCommand.php │ ├── Component.php │ ├── Components │ ├── ActionRow.php │ ├── ButtonComponent.php │ ├── GenericButtonComponent.php │ ├── GenericTextInputComponent.php │ ├── LinkButtonComponent.php │ ├── ParagraphTextInputComponent.php │ ├── SelectMenuComponent.php │ └── ShortTextInputComponent.php │ ├── Embed.php │ ├── Embeds │ ├── AuthorEmbed.php │ ├── FieldEmbed.php │ ├── FooterEmbed.php │ ├── GenericEmbed.php │ ├── ImageEmbed.php │ ├── ProviderEmbed.php │ ├── ThumbnailEmbed.php │ └── VideoEmbed.php │ ├── Interactions │ ├── DiscordInteractionResponse.php │ ├── Handlers │ │ ├── ApplicationCommandHandler.php │ │ ├── MessageComponentInteractionHandler.php │ │ └── PingHandler.php │ └── InteractionHandler.php │ ├── Objects │ ├── AllowedMentionObject.php │ ├── EmojiObject.php │ └── SelectOptionObject.php │ ├── SupportObject.php │ └── Traits │ ├── ApplicationCommand │ ├── HasAutoComplete.php │ ├── HasChoices.php │ ├── HasOptions.php │ └── NoChoiceTransformer.php │ ├── DiscordApiService.php │ ├── FiltersRecursive.php │ ├── HasEmojiObject.php │ ├── HasInteractionListeners.php │ └── MergesArrays.php └── tests ├── TestCase.php ├── Traits ├── BasicCommandOptionTests.php └── BasicCommandTests.php └── Unit ├── Channels └── DiscordNotificationChannelTest.php ├── Events ├── ApplicationCommandInteractionEventTest.php └── MessageComponentInteractionEventTest.php ├── Services ├── DiscordApiServiceTest.php ├── DiscordApplicationCommandServiceTest.php └── DiscordInteractionServiceTest.php └── Support ├── Builder ├── ComponentBuilderTest.php └── EmbedBuilderTest.php ├── Commands ├── MessageCommandTest.php ├── Options │ ├── AttachmentOptionTest.php │ ├── BooleanOptionTest.php │ ├── ChannelOptionTest.php │ ├── IntegerOptionTest.php │ ├── MentionableOptionTest.php │ ├── NumberOptionTest.php │ ├── OptionChoiceTest.php │ ├── RoleOptionTest.php │ ├── StringOptionTest.php │ ├── SubCommandGroupOptionTest.php │ ├── SubCommandOptionTest.php │ └── UserOptionTest.php ├── SlashCommandTest.php └── UserCommandTest.php ├── Components ├── ActionRowTest.php ├── ButtonComponentTest.php ├── LinkButtonComponentTest.php ├── ParagraphTextInputComponentTest.php ├── SelectMenuComponentTest.php └── ShortTextInputComponentTest.php ├── Embeds ├── AuthorEmbedTest.php ├── FieldEmbedTest.php ├── FooterEmbedTest.php ├── GenericEmbedTest.php ├── ImageEmbedTest.php ├── ProviderEmbedTest.php ├── ThumbnailEmbedTest.php └── VideoEmbedTest.php ├── Interactions ├── DiscordInteractionResponseTest.php └── Handlers │ ├── ApplicationCommandHandlerTest.php │ ├── MessageComponentInteractionHandlerTest.php │ └── PingHandlerTest.php └── Objects ├── AllowedMentionObjectTest.php ├── EmojiObjectTest.php └── SelectOptionObjectTest.php /.github/coverage-badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | coverage 10 | coverage 11 | 12 | 13 | 95 % 14 | 95 % 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | run-coverage: 7 | name: Run Tests with Coverage 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Composer 15 | uses: php-actions/composer@v5 16 | with: 17 | php_extensions: xdebug 18 | - name: Run Tests 19 | env: 20 | XDEBUG_MODE: coverage 21 | run: vendor/bin/phpunit 22 | - name: Commit Coverage 23 | uses: timkrase/phpunit-coverage-badge@v1.2.0 24 | with: 25 | report: clover.xml 26 | report_type: clover 27 | coverage_badge_path: ./.github/coverage-badge.svg 28 | repo_token: ${{ secrets.GH_ACCESS_TOKEN }} 29 | push_badge: true 30 | -------------------------------------------------------------------------------- /.github/workflows/develop-branch.yml: -------------------------------------------------------------------------------- 1 | name: Develop Branch 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | 7 | jobs: 8 | run-tests: 9 | name: Tests 10 | uses: ./.github/workflows/test.yml 11 | run-coverage: 12 | name: Coverage 13 | uses: ./.github/workflows/coverage.yml 14 | -------------------------------------------------------------------------------- /.github/workflows/main-branch.yml: -------------------------------------------------------------------------------- 1 | name: Main Branch 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | run-tests: 9 | name: Tests 10 | uses: ./.github/workflows/test.yml 11 | run-coverage: 12 | name: Coverage 13 | uses: ./.github/workflows/coverage.yml 14 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | test: 7 | name: Run Tests 8 | uses: ./.github/workflows/test.yml 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | workflow_call: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | php: ["7.4", "8.0", "8.1"] 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Composer 17 | uses: php-actions/composer@v5 18 | with: 19 | php_version: ${{ matrix.php }} 20 | - name: Run Tests 21 | run: vendor/bin/phpunit --no-coverage 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .env 3 | .phpunit.result.cache 4 | tests/html-coverage 5 | clover.xml 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 nwilging 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nwilging/laravel-discord-bot", 3 | "description": "A robust Discord messaging integration for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Nwilging\\LaravelDiscordBot\\": "src/", 9 | "Nwilging\\LaravelDiscordBotTests\\": "tests/" 10 | } 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Nicole Wilging", 15 | "email": "nicole@wilging.com" 16 | } 17 | ], 18 | "require": { 19 | "ext-sodium": "*", 20 | "php": ">=7.4", 21 | "laravel/framework": ">=8", 22 | "guzzlehttp/guzzle": "^7.4" 23 | }, 24 | "extra": { 25 | "laravel": { 26 | "providers": [ 27 | "Nwilging\\LaravelDiscordBot\\Providers\\DiscordBotServiceProvider" 28 | ] 29 | } 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^9.5", 33 | "mockery/mockery": "^1.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/discord.php: -------------------------------------------------------------------------------- 1 | env('DISCORD_API_BOT_TOKEN'), 11 | 'api_url' => env('DISCORD_API_URL', 'https://discord.com/api'), 12 | 'application_id' => env('DISCORD_APPLICATION_ID'), 13 | 'public_key' => env('DISCORD_PUBLIC_KEY'), 14 | 'interactions' => [ 15 | 'component_interaction_default_behavior' => (in_array( 16 | env('DISCORD_COMPONENT_INTERACTION_DEFAULT_BEHAVIOR'), 17 | $allowedDefaultBehaviorTypes 18 | )) ? env('DISCORD_COMPONENT_INTERACTION_DEFAULT_BEHAVIOR') : 'defer', 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Channels/DiscordNotificationChannel.php: -------------------------------------------------------------------------------- 1 | discordApiService = $discordApiService; 17 | } 18 | 19 | public function send($notifiable, DiscordNotificationContract $notification): array 20 | { 21 | $notificationArray = $notification->toDiscord($notifiable); 22 | switch ($notificationArray['contentType']) { 23 | case 'plain': 24 | return $this->handleTextMessage($notificationArray); 25 | case 'rich': 26 | return $this->handleRichTextMessage($notificationArray); 27 | default: 28 | throw new \InvalidArgumentException(sprintf('%s is not a valid contentType', $notificationArray['contentType'])); 29 | } 30 | } 31 | 32 | protected function handleTextMessage(array $notificationArray): array 33 | { 34 | $channelId = $notificationArray['channelId']; 35 | $message = $notificationArray['message']; 36 | $options = $notificationArray['options'] ?? []; 37 | 38 | return $this->discordApiService->sendTextMessage($channelId, $message, $options); 39 | } 40 | 41 | protected function handleRichTextMessage(array $notificationArray): array 42 | { 43 | $channelId = $notificationArray['channelId']; 44 | $embeds = $notificationArray['embeds'] ?? []; 45 | $components = $notificationArray['components'] ?? []; 46 | $options = $notificationArray['options'] ?? []; 47 | 48 | return $this->discordApiService->sendRichTextMessage($channelId, $embeds, $components, $options); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Contracts/Channels/DiscordNotificationChannelContract.php: -------------------------------------------------------------------------------- 1 | interactionRequest = $interactionRequest; 18 | } 19 | 20 | public function getInteractionRequest(): ParameterBag 21 | { 22 | return $this->interactionRequest; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Events/ApplicationCommandInteractionEvent.php: -------------------------------------------------------------------------------- 1 | interactionRequest->get('data', []); 11 | } 12 | 13 | public function getCommandName(): string 14 | { 15 | return $this->getData()['name']; 16 | } 17 | 18 | public function getCommandId(): string 19 | { 20 | return $this->getData()['id']; 21 | } 22 | 23 | public function getCommandType(): int 24 | { 25 | return (int) $this->getData()['type']; 26 | } 27 | 28 | public function getChannelId(): string 29 | { 30 | return $this->interactionRequest->get('channel_id'); 31 | } 32 | 33 | public function getApplicationId(): string 34 | { 35 | return $this->interactionRequest->get('application_id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Events/MessageComponentInteractionEvent.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../../config/discord.php', 'discord'); 28 | } 29 | 30 | public function register() 31 | { 32 | Notification::resolved(function (ChannelManager $channelManager): void { 33 | $channelManager->extend('discord', function (): DiscordNotificationChannelContract { 34 | return $this->app->make(DiscordNotificationChannelContract::class); 35 | }); 36 | }); 37 | 38 | $this->app->bind(ClientInterface::class, Client::class); 39 | 40 | $this->app->bind(DiscordApiServiceContract::class, DiscordApiService::class); 41 | $this->app->when(DiscordApiService::class)->needs('$token')->give(function (): string { 42 | return $this->app->make(Config::class)->get('discord.token'); 43 | }); 44 | 45 | $this->app->when(DiscordApiService::class)->needs('$apiUrl')->give(function (): string { 46 | return $this->app->make(Config::class)->get('discord.api_url'); 47 | }); 48 | 49 | $this->app->bind(DiscordInteractionServiceContract::class, DiscordInteractionService::class); 50 | $this->app->when(DiscordInteractionService::class)->needs('$applicationId')->give(function (): string { 51 | return $this->app->make(Config::class)->get('discord.application_id'); 52 | }); 53 | 54 | $this->app->when(DiscordInteractionService::class)->needs('$publicKey')->give(function (): string { 55 | return $this->app->make(Config::class)->get('discord.public_key'); 56 | }); 57 | 58 | $this->app->when(MessageComponentInteractionHandler::class)->needs('$defaultBehavior')->give(function (): string { 59 | return $this->app->make(Config::class)->get('discord.interactions.component_interaction_default_behavior'); 60 | }); 61 | 62 | $this->app->when(ApplicationCommandHandler::class)->needs('$defaultBehavior')->give(function (): string { 63 | return $this->app->make(Config::class)->get('discord.interactions.component_interaction_default_behavior'); 64 | }); 65 | 66 | $this->app->bind(DiscordNotificationChannelContract::class, DiscordNotificationChannel::class); 67 | 68 | $this->app->bind(DiscordApplicationCommandServiceContract::class, DiscordApplicationCommandService::class); 69 | 70 | $this->app->when(DiscordApplicationCommandService::class)->needs('$token')->give(function (): string { 71 | return $this->app->make(Config::class)->get('discord.token'); 72 | }); 73 | 74 | $this->app->when(DiscordApplicationCommandService::class)->needs('$apiUrl')->give(function (): string { 75 | return $this->app->make(Config::class)->get('discord.api_url'); 76 | }); 77 | 78 | $this->app->when(DiscordApplicationCommandService::class)->needs('$applicationId')->give(function (): string { 79 | return $this->app->make(Config::class)->get('discord.application_id'); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Services/DiscordApiService.php: -------------------------------------------------------------------------------- 1 | token = $token; 19 | $this->apiUrl = $apiUrl; 20 | $this->httpClient = $httpClient; 21 | } 22 | 23 | public function sendTextMessage(string $channelId, string $message, array $options = []): array 24 | { 25 | $response = $this->makeRequest( 26 | 'POST', 27 | sprintf('channels/%s/messages', $channelId), 28 | array_merge($this->buildMessageOptions($options), [ 29 | 'content' => $message, 30 | ]), 31 | ); 32 | 33 | return json_decode($response->getBody()->getContents(), true); 34 | } 35 | 36 | public function sendRichTextMessage(string $channelId, array $embeds, array $components = [], array $options = []): array 37 | { 38 | $embedArrays = array_map(function (Embed $embed): array { 39 | return $embed->toArray(); 40 | }, $embeds); 41 | 42 | $componentArrays = array_map(function (Component $component): array { 43 | return $component->toArray(); 44 | }, $components); 45 | 46 | $response = $this->makeRequest( 47 | 'POST', 48 | sprintf('channels/%s/messages', $channelId), 49 | array_merge($this->buildMessageOptions($options), [ 50 | 'embeds' => $embedArrays, 51 | 'components' => $componentArrays, 52 | ]), 53 | ); 54 | 55 | return json_decode($response->getBody()->getContents(), true); 56 | } 57 | 58 | protected function buildMessageOptions(array $options): array 59 | { 60 | return []; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Services/DiscordApplicationCommandService.php: -------------------------------------------------------------------------------- 1 | applicationId = $applicationId; 21 | $this->token = $token; 22 | $this->apiUrl = $apiUrl; 23 | $this->httpClient = $httpClient; 24 | } 25 | 26 | public function createGlobalCommand(Command $command): array 27 | { 28 | $response = $this->makeRequest( 29 | Request::METHOD_POST, 30 | sprintf('applications/%s/commands', $this->applicationId), 31 | $command->toArray(), 32 | ); 33 | 34 | return json_decode($response->getBody()->getContents(), true); 35 | } 36 | 37 | public function createGuildCommand(string $guildId, Command $command): array 38 | { 39 | $response = $this->makeRequest( 40 | Request::METHOD_POST, 41 | sprintf('applications/%s/guilds/%s/commands', $this->applicationId, $guildId), 42 | $command->toArray(), 43 | ); 44 | 45 | return json_decode($response->getBody()->getContents(), true); 46 | } 47 | 48 | public function deleteGlobalCommand(string $commandId): void 49 | { 50 | $this->makeRequest( 51 | Request::METHOD_DELETE, 52 | sprintf('applications/%s/commands/%s', $this->applicationId, $commandId) 53 | ); 54 | } 55 | 56 | public function deleteGuildCommand(string $guildId, string $commandId): void 57 | { 58 | $this->makeRequest( 59 | Request::METHOD_DELETE, 60 | sprintf('applications/%s/guilds/%s/commands/%s', $this->applicationId, $guildId, $commandId) 61 | ); 62 | } 63 | 64 | public function updateGlobalCommand(string $commandId, Command $command): array 65 | { 66 | $response = $this->makeRequest( 67 | Request::METHOD_PATCH, 68 | sprintf('applications/%s/commands/%s', $this->applicationId, $commandId), 69 | $command->toArray() 70 | ); 71 | 72 | return json_decode($response->getBody()->getContents(), true); 73 | } 74 | 75 | public function updateGuildCommand(string $guildId, string $commandId, Command $command): array 76 | { 77 | $response = $this->makeRequest( 78 | Request::METHOD_PATCH, 79 | sprintf('applications/%s/guilds/%s/commands/%s', $this->applicationId, $guildId, $commandId), 80 | $command->toArray() 81 | ); 82 | 83 | return json_decode($response->getBody()->getContents(), true); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Services/DiscordInteractionService.php: -------------------------------------------------------------------------------- 1 | PingHandler::class, 28 | InteractionHandler::REQUEST_TYPE_APPLICATION_COMMAND => ApplicationCommandHandler::class, 29 | InteractionHandler::REQUEST_TYPE_MESSAGE_COMPONENT => MessageComponentInteractionHandler::class, 30 | ]; 31 | 32 | public function __construct(string $applicationId, string $publicKey, Application $laravel) 33 | { 34 | $this->applicationId = $applicationId; 35 | $this->publicKey = $publicKey; 36 | $this->laravel = $laravel; 37 | } 38 | 39 | public function handleInteractionRequest(Request $request): DiscordInteractionResponse 40 | { 41 | $this->validate($request); 42 | $json = $request->json()->all(); 43 | 44 | $handlerClass = $this->interactionHandlers[$json['type']] ?? null; 45 | if (!$handlerClass) { 46 | throw new NotFoundHttpException(); 47 | } 48 | 49 | $handler = $this->laravel->make($handlerClass); 50 | return $handler->handle($request); 51 | } 52 | 53 | protected function validate(Request $request): void 54 | { 55 | $signature = $request->header('X-Signature-Ed25519'); 56 | $timestamp = $request->header('X-Signature-Timestamp'); 57 | $body = $request->getContent(); 58 | 59 | if (!$signature || !$timestamp || !$body) { 60 | throw new UnauthorizedHttpException('invalid request signature'); 61 | } 62 | 63 | $data = sprintf('%s%s', $timestamp, $body); 64 | try { 65 | $verified = sodium_crypto_sign_verify_detached(hex2bin($signature), $data, hex2bin($this->publicKey)); 66 | } catch (\SodiumException $exception) { 67 | throw new UnauthorizedHttpException('invalid request signature'); 68 | } 69 | 70 | if (!$verified) { 71 | throw new UnauthorizedHttpException('invalid request signature'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Support/Builder/ComponentBuilder.php: -------------------------------------------------------------------------------- 1 | components[] = $component; 22 | return $this; 23 | } 24 | 25 | public function addActionButton(string $label, string $customId): self 26 | { 27 | $this->components[] = new ButtonComponent($label, $customId); 28 | return $this; 29 | } 30 | 31 | public function addLinkButton(string $label, string $url): self 32 | { 33 | $this->components[] = new LinkButtonComponent($label, $url); 34 | return $this; 35 | } 36 | 37 | /** 38 | * @param SelectOptionObject[] $options 39 | * @param string $customId 40 | * @return SelectMenuComponent 41 | */ 42 | public function addSelectMenuComponent(array $options, string $customId): self 43 | { 44 | $this->components[] = new SelectMenuComponent($customId, $options); 45 | return $this; 46 | } 47 | 48 | public function withSelectOptionObject(string $label, string $value): SelectOptionObject 49 | { 50 | return new SelectOptionObject($label, $value); 51 | } 52 | 53 | public function addShortTextInput(string $label, string $customId): self 54 | { 55 | $this->components[] = new ShortTextInputComponent($label, $customId); 56 | return $this; 57 | } 58 | 59 | public function addParagraphTextInput(string $label, string $customId): self 60 | { 61 | $this->components[] = new ParagraphTextInputComponent($label, $customId); 62 | return $this; 63 | } 64 | 65 | public function getActionRow(): ActionRow 66 | { 67 | return new ActionRow($this->components); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Support/Builder/EmbedBuilder.php: -------------------------------------------------------------------------------- 1 | embeds[] = $embed; 25 | return $this; 26 | } 27 | 28 | public function addFooter(string $text): self 29 | { 30 | $this->addEmbed(new FooterEmbed($text)); 31 | return $this; 32 | } 33 | 34 | public function addImage(string $url): self 35 | { 36 | $this->addEmbed(new ImageEmbed($url)); 37 | return $this; 38 | } 39 | 40 | public function addThumbnail(string $url): self 41 | { 42 | $this->addEmbed(new ThumbnailEmbed($url)); 43 | return $this; 44 | } 45 | 46 | public function addVideo(string $url): self 47 | { 48 | $this->addEmbed(new VideoEmbed($url)); 49 | return $this; 50 | } 51 | 52 | public function addProvider(string $name, string $url): self 53 | { 54 | $embed = new ProviderEmbed(); 55 | $embed->withName($name)->withUrl($url); 56 | 57 | $this->addEmbed($embed); 58 | return $this; 59 | } 60 | 61 | public function addAuthor(string $name): self 62 | { 63 | $this->addEmbed(new AuthorEmbed($name)); 64 | return $this; 65 | } 66 | 67 | public function getEmbeds(): array 68 | { 69 | return $this->embeds; 70 | } 71 | 72 | public function toArray(): array 73 | { 74 | return array_map(function (Embed $embed): array { 75 | return $embed->toArray(); 76 | }, $this->embeds); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Support/Command.php: -------------------------------------------------------------------------------- 1 | name = $name; 46 | } 47 | 48 | public function parentApplication(string $applicationId): self 49 | { 50 | $this->parentApplicationId = $applicationId; 51 | return $this; 52 | } 53 | 54 | public function nameLocalizations(array $localizations): self 55 | { 56 | $this->nameLocalizations = $localizations; 57 | return $this; 58 | } 59 | 60 | public function descriptionLocalizations(array $localizations): self 61 | { 62 | $this->descriptionLocalizations = $localizations; 63 | return $this; 64 | } 65 | 66 | /** 67 | * Set of permissions represented as a bit set 68 | * 69 | * @see https://discord.com/developers/docs/topics/permissions 70 | * 71 | * @param string $permission 72 | * @return $this 73 | */ 74 | public function defaultMemberPermissions(string $permission): self 75 | { 76 | $this->defaultMemberPermissions = $permission; 77 | return $this; 78 | } 79 | 80 | public function dmPermission(bool $enable = true): self 81 | { 82 | $this->dmPermission = $enable; 83 | return $this; 84 | } 85 | 86 | public function defaultPermission(bool $enable = true): self 87 | { 88 | $this->defaultPermission = $enable; 89 | return $this; 90 | } 91 | 92 | public function nsfw(bool $enable = true): self 93 | { 94 | $this->nsfw = $enable; 95 | return $this; 96 | } 97 | 98 | public function version(string $version): self 99 | { 100 | $this->version = $version; 101 | return $this; 102 | } 103 | 104 | public abstract function getType(): int; 105 | 106 | public function toArray(): array 107 | { 108 | return $this->arrayFilterRecursive([ 109 | 'type' => $this->getType(), 110 | 'name' => $this->name, 111 | 'application_id' => $this->parentApplicationId, 112 | 'name_localizations' => $this->nameLocalizations, 113 | 'description_localizations' => $this->descriptionLocalizations, 114 | 'default_member_permissions' => $this->defaultMemberPermissions, 115 | 'dm_permission' => $this->dmPermission, 116 | 'default_permission' => $this->defaultPermission, 117 | 'nsfw' => $this->nsfw, 118 | 'version' => $this->version, 119 | ]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Support/Commands/CommandOption.php: -------------------------------------------------------------------------------- 1 | name = $name; 52 | $this->description = $description; 53 | } 54 | 55 | public abstract function getType(): int; 56 | 57 | protected abstract function choiceTransformer(OptionChoice $choice): array; 58 | 59 | /** 60 | * If the parameter is required or optional 61 | * 62 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 63 | * 64 | * @param bool $required 65 | * @return $this 66 | */ 67 | public function required(bool $required = true): self 68 | { 69 | $this->required = $required; 70 | return $this; 71 | } 72 | 73 | /** 74 | * Localization dictionary for the `name` field. Values follow the same restrictions as `name` 75 | * 76 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 77 | * 78 | * @param array $localizations 79 | * @return $this 80 | */ 81 | public function nameLocalizations(array $localizations): self 82 | { 83 | $this->nameLocalizations = $localizations; 84 | return $this; 85 | } 86 | 87 | /** 88 | * Localization dictionary for the `description` field. Values follow the same restrictions as `description` 89 | * 90 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 91 | * 92 | * @param array $localizations 93 | * @return $this 94 | */ 95 | public function descriptionLocalizations(array $localizations): self 96 | { 97 | $this->descriptionLocalizations = $localizations; 98 | return $this; 99 | } 100 | 101 | public function toArray(): array 102 | { 103 | return $this->arrayFilterRecursive([ 104 | 'type' => $this->getType(), 105 | 'name' => $this->name, 106 | 'description' => $this->description, 107 | 'required' => $this->required, 108 | 'name_localizations' => $this->nameLocalizations, 109 | 'description_localizations' => $this->descriptionLocalizations, 110 | ]); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Support/Commands/MessageCommand.php: -------------------------------------------------------------------------------- 1 | channelTypes = $types; 30 | return $this; 31 | } 32 | 33 | public function getType(): int 34 | { 35 | return static::TYPE_CHANNEL; 36 | } 37 | 38 | public function toArray(): array 39 | { 40 | return $this->toMergedArray([ 41 | 'channel_types' => $this->channelTypes, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/Commands/Options/IntegerOption.php: -------------------------------------------------------------------------------- 1 | minValue = $minValue; 35 | return $this; 36 | } 37 | 38 | /** 39 | * The maximum value permitted 40 | * 41 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 42 | * 43 | * @param int $maxValue 44 | * @return $this 45 | */ 46 | public function maxValue(int $maxValue): self 47 | { 48 | $this->maxValue = $maxValue; 49 | return $this; 50 | } 51 | 52 | protected function choiceTransformer(OptionChoice $choice): array 53 | { 54 | $array = $choice->toArray(); 55 | $array['value'] = intval($array['value']); 56 | 57 | return $array; 58 | } 59 | 60 | public function toArray(): array 61 | { 62 | $merge = $this->mergeChoices([]); 63 | $merge = $this->mergeAutocomplete($merge); 64 | 65 | return $this->toMergedArray(array_merge($merge, [ 66 | 'min_value' => $this->minValue, 67 | 'max_value' => $this->maxValue, 68 | ])); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Support/Commands/Options/MentionableOption.php: -------------------------------------------------------------------------------- 1 | toArray(); 27 | $array['value'] = (float) $array['value']; 28 | 29 | return $array; 30 | } 31 | 32 | /** 33 | * The minimum value permitted 34 | * 35 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 36 | * 37 | * @param float $minValue 38 | * @return $this 39 | */ 40 | public function minValue(float $minValue): self 41 | { 42 | $this->minValue = $minValue; 43 | return $this; 44 | } 45 | 46 | /** 47 | * The maximum value permitted 48 | * 49 | * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure 50 | * 51 | * @param float $maxValue 52 | * @return $this 53 | */ 54 | public function maxValue(float $maxValue): self 55 | { 56 | $this->maxValue = $maxValue; 57 | return $this; 58 | } 59 | 60 | public function toArray(): array 61 | { 62 | $merge = $this->mergeChoices([]); 63 | $merge = $this->mergeAutocomplete($merge); 64 | 65 | return $this->toMergedArray(array_merge($merge, [ 66 | 'min_value' => $this->minValue, 67 | 'max_value' => $this->maxValue, 68 | ])); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Support/Commands/Options/OptionChoice.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->value = $value; 27 | } 28 | 29 | public function nameLocalizations(array $localizations): self 30 | { 31 | $this->nameLocalizations = $localizations; 32 | return $this; 33 | } 34 | 35 | public function toArray(): array 36 | { 37 | return $this->arrayFilterRecursive([ 38 | 'name' => $this->name, 39 | 'name_localizations' => $this->nameLocalizations, 40 | 'value' => $this->value, 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Support/Commands/Options/RoleOption.php: -------------------------------------------------------------------------------- 1 | maxLength = $maxLength; 25 | return $this; 26 | } 27 | 28 | protected function choiceTransformer(OptionChoice $choice): array 29 | { 30 | $array = $choice->toArray(); 31 | $array['value'] = (string) $array['value']; 32 | 33 | return $array; 34 | } 35 | 36 | public function toArray(): array 37 | { 38 | $merge = $this->mergeChoices([]); 39 | $merge = $this->mergeAutocomplete($merge); 40 | 41 | return $this->toMergedArray(array_merge($merge, [ 42 | 'max_length' => $this->maxLength, 43 | ])); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/Commands/Options/SubCommandGroupOption.php: -------------------------------------------------------------------------------- 1 | description = $description; 20 | } 21 | 22 | public function getType(): int 23 | { 24 | return static::TYPE_CHAT_INPUT; 25 | } 26 | 27 | public function toArray(): array 28 | { 29 | return $this->toMergedArray($this->mergeOptions([ 30 | 'description' => $this->description, 31 | ])); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Support/Commands/UserCommand.php: -------------------------------------------------------------------------------- 1 | customId = $customId; 27 | } 28 | 29 | public abstract function getType(): int; 30 | 31 | /** 32 | * Returns a Discord-API compliant component array 33 | * 34 | * @see https://discord.com/developers/docs/interactions/message-components#component-object 35 | * 36 | * @return array 37 | */ 38 | public function toArray(): array 39 | { 40 | return $this->arrayFilterRecursive([ 41 | 'type' => $this->getType(), 42 | 'custom_id' => $this->customId, 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Support/Components/ActionRow.php: -------------------------------------------------------------------------------- 1 | components = $components; 25 | } 26 | 27 | public function addComponent(Component $component): self 28 | { 29 | $this->components[] = $component; 30 | return $this; 31 | } 32 | 33 | public function getType(): int 34 | { 35 | return static::TYPE_ACTION_ROW; 36 | } 37 | 38 | public function toArray(): array 39 | { 40 | return $this->toMergedArray([ 41 | 'components' => array_map(function (Component $component): array { 42 | return $component->toArray(); 43 | }, $this->components), 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Support/Components/ButtonComponent.php: -------------------------------------------------------------------------------- 1 | style = static::STYLE_PRIMARY; 31 | return $this; 32 | } 33 | 34 | /** 35 | * Sets the button style to secondary 36 | * 37 | * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles 38 | * @return $this 39 | */ 40 | public function withSecondaryStyle(): self 41 | { 42 | $this->style = static::STYLE_SECONDARY; 43 | return $this; 44 | } 45 | 46 | /** 47 | * Sets the button style to success 48 | * 49 | * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles 50 | * @return $this 51 | */ 52 | public function withSuccessStyle(): self 53 | { 54 | $this->style = static::STYLE_SUCCESS; 55 | return $this; 56 | } 57 | 58 | /** 59 | * Sets the button style to danger 60 | * 61 | * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-styles 62 | * @return $this 63 | */ 64 | public function withDangerStyle(): self 65 | { 66 | $this->style = static::STYLE_DANGER; 67 | return $this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Support/Components/GenericButtonComponent.php: -------------------------------------------------------------------------------- 1 | style = $style; 31 | $this->label = $label; 32 | } 33 | 34 | /** 35 | * Whether the button is disabled 36 | * 37 | * @see https://discord.com/developers/docs/interactions/message-components#button-object-button-structure 38 | * @param bool $disabled 39 | * @return $this 40 | */ 41 | public function disabled(bool $disabled = true): self 42 | { 43 | $this->disabled = $disabled; 44 | return $this; 45 | } 46 | 47 | public function getType(): int 48 | { 49 | return static::TYPE_BUTTON; 50 | } 51 | 52 | public function toArray(): array 53 | { 54 | return $this->arrayFilterRecursive($this->mergeEmojiObject([ 55 | 'type' => $this->getType(), 56 | 'custom_id' => $this->customId, 57 | 'style' => $this->style, 58 | 'label' => $this->label, 59 | 'disabled' => $this->disabled, 60 | ])); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Support/Components/GenericTextInputComponent.php: -------------------------------------------------------------------------------- 1 | style = $style; 35 | $this->label = $label; 36 | } 37 | 38 | /** 39 | * The minimum input length for a text input, min 0, max 4000 40 | * 41 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 42 | * @param int $minLength 43 | * @return $this 44 | */ 45 | public function withMinLength(int $minLength): self 46 | { 47 | $this->minLength = $minLength; 48 | return $this; 49 | } 50 | 51 | /** 52 | * The maximum input length for a text input, min 1, max 4000 53 | * 54 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 55 | * @param int $maxLength 56 | * @return $this 57 | */ 58 | public function withMaxLength(int $maxLength): self 59 | { 60 | $this->maxLength = $maxLength; 61 | return $this; 62 | } 63 | 64 | /** 65 | * Custom placeholder text if the input is empty, max 100 characters 66 | * 67 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 68 | * @param string $placeholder 69 | * @return $this 70 | */ 71 | public function withPlaceholder(string $placeholder): self 72 | { 73 | $this->placeholder = $placeholder; 74 | return $this; 75 | } 76 | 77 | /** 78 | * A pre-filled value for this component, max 4000 characters 79 | * 80 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 81 | * @param string $value 82 | * @return $this 83 | */ 84 | public function withValue(string $value): self 85 | { 86 | $this->value = $value; 87 | return $this; 88 | } 89 | 90 | /** 91 | * Whether this component is required to be filled, default true 92 | * 93 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 94 | * @param bool $required 95 | * @return $this 96 | */ 97 | public function required(bool $required = true): self 98 | { 99 | $this->required = $required; 100 | return $this; 101 | } 102 | 103 | public function getType(): int 104 | { 105 | return static::TYPE_TEXT_INPUT; 106 | } 107 | 108 | /** 109 | * Returns a Discord-API compatible text input array 110 | * 111 | * @see https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure 112 | * @return array 113 | */ 114 | public function toArray(): array 115 | { 116 | return $this->arrayFilterRecursive([ 117 | 'type' => $this->getType(), 118 | 'custom_id' => $this->customId, 119 | 'style' => $this->style, 120 | 'label' => $this->label, 121 | 'min_length' => $this->minLength, 122 | 'max_length' => $this->maxLength, 123 | 'required' => $this->required, 124 | 'value' => $this->value, 125 | 'placeholder' => $this->placeholder, 126 | ]); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Support/Components/LinkButtonComponent.php: -------------------------------------------------------------------------------- 1 | url = $url; 22 | } 23 | 24 | public function toArray(): array 25 | { 26 | return $this->toMergedArray([ 27 | 'url' => $this->url, 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Support/Components/ParagraphTextInputComponent.php: -------------------------------------------------------------------------------- 1 | options = $options; 35 | } 36 | 37 | /** 38 | * Custom placeholder text if nothing is selected, max 150 characters 39 | * 40 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure 41 | * @param string $placeholder 42 | * @return $this 43 | */ 44 | public function withPlaceholder(string $placeholder): self 45 | { 46 | $this->placeholder = $placeholder; 47 | return $this; 48 | } 49 | 50 | /** 51 | * The minimum number of items that must be chosen; default 1, min 0, max 25 52 | * 53 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure 54 | * @param int $minValues 55 | * @return $this 56 | */ 57 | public function withMinValues(int $minValues): self 58 | { 59 | $this->minValues = $minValues; 60 | return $this; 61 | } 62 | 63 | /** 64 | * The maximum number of items that can be chosen; default 1, max 25 65 | * 66 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure 67 | * @param int $maxValues 68 | * @return $this 69 | */ 70 | public function withMaxValues(int $maxValues): self 71 | { 72 | $this->maxValues = $maxValues; 73 | return $this; 74 | } 75 | 76 | /** 77 | * Disables the select 78 | * 79 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure 80 | * @param bool $disabled 81 | * @return $this 82 | */ 83 | public function disabled(bool $disabled = true): self 84 | { 85 | $this->disabled = $disabled; 86 | return $this; 87 | } 88 | 89 | public function getType(): int 90 | { 91 | return static::TYPE_SELECT_MENU; 92 | } 93 | 94 | public function toArray(): array 95 | { 96 | return $this->toMergedArray([ 97 | 'options' => array_map(function (SelectOptionObject $option): array { 98 | return $option->toArray(); 99 | }, $this->options), 100 | 'placeholder' => $this->placeholder, 101 | 'min_values' => $this->minValues, 102 | 'max_values' => $this->maxValues, 103 | 'disabled' => $this->disabled, 104 | ]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Support/Components/ShortTextInputComponent.php: -------------------------------------------------------------------------------- 1 | title = $title; 40 | $this->description = $description; 41 | $this->timestamp = $timestamp; 42 | } 43 | 44 | /** 45 | * The color code of the embed 46 | * 47 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-structure 48 | * 49 | * @param int $colorCode 50 | * @return $this 51 | */ 52 | public function withColor(int $colorCode): self 53 | { 54 | $this->color = $colorCode; 55 | return $this; 56 | } 57 | 58 | public abstract function getType(): string; 59 | 60 | /** 61 | * Returns a Discord-API compliant embed array 62 | * 63 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-structure 64 | * 65 | * @return array 66 | */ 67 | public function toArray(): array 68 | { 69 | return $this->arrayFilterRecursive([ 70 | 'type' => $this->getType(), 71 | 'title' => $this->title, 72 | 'description' => $this->description, 73 | 'timestamp' => $this->timestamp, 74 | 'color' => $this->color, 75 | ]); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Support/Embeds/AuthorEmbed.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | } 30 | 31 | /** 32 | * URL of author 33 | * 34 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure 35 | * @param string $url 36 | * @return $this 37 | */ 38 | public function withUrl(string $url): self 39 | { 40 | $this->url = $url; 41 | return $this; 42 | } 43 | 44 | /** 45 | * URL of author icon (only supports http(s) and attachments) 46 | * 47 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure 48 | * @param string $iconUrl 49 | * @return $this 50 | */ 51 | public function withIconUrl(string $iconUrl): self 52 | { 53 | $this->iconUrl = $iconUrl; 54 | return $this; 55 | } 56 | 57 | /** 58 | * A proxied url of author icon 59 | * 60 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure 61 | * @param string $proxyIconUrl 62 | * @return $this 63 | */ 64 | public function withProxyIconUrl(string $proxyIconUrl): self 65 | { 66 | $this->proxyIconUrl = $proxyIconUrl; 67 | return $this; 68 | } 69 | 70 | public function getType(): string 71 | { 72 | return static::TYPE_AUTHOR; 73 | } 74 | 75 | public function toArray(): array 76 | { 77 | return $this->toMergedArray([ 78 | 'author' => [ 79 | 'name' => $this->name, 80 | 'url' => $this->url, 81 | 'icon_url' => $this->iconUrl, 82 | 'proxy_icon_url' => $this->proxyIconUrl, 83 | ], 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Support/Embeds/FieldEmbed.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->value = $value; 29 | } 30 | 31 | /** 32 | * Whether this field should display inline 33 | * 34 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure 35 | * @param bool $inline 36 | * @return $this 37 | */ 38 | public function inline(bool $inline = true): self 39 | { 40 | $this->inline = $inline; 41 | return $this; 42 | } 43 | 44 | public function getType(): string 45 | { 46 | return static::TYPE_FIELD; 47 | } 48 | 49 | public function toArray(): array 50 | { 51 | return $this->toMergedArray([ 52 | 'field' => [ 53 | 'name' => $this->name, 54 | 'value' => $this->value, 55 | 'inline' => $this->inline, 56 | ], 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Support/Embeds/FooterEmbed.php: -------------------------------------------------------------------------------- 1 | text = $text; 27 | } 28 | 29 | /** 30 | * URL of footer icon (only supports http(s) and attachments) 31 | * 32 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure 33 | * @param string $iconUrl 34 | * @return $this 35 | */ 36 | public function withIconUrl(string $iconUrl): self 37 | { 38 | $this->iconUrl = $iconUrl; 39 | return $this; 40 | } 41 | 42 | /** 43 | * A proxied url of footer icon 44 | * 45 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure 46 | * @param string $proxyIconUrl 47 | * @return $this 48 | */ 49 | public function withProxyIconUrl(string $proxyIconUrl): self 50 | { 51 | $this->proxyIconUrl = $proxyIconUrl; 52 | return $this; 53 | } 54 | 55 | public function getType(): string 56 | { 57 | return static::TYPE_FOOTER; 58 | } 59 | 60 | public function toArray(): array 61 | { 62 | return $this->toMergedArray([ 63 | 'footer' => [ 64 | 'text' => $this->text, 65 | 'icon_url' => $this->iconUrl, 66 | 'proxy_icon_url' => $this->proxyIconUrl, 67 | ], 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Support/Embeds/GenericEmbed.php: -------------------------------------------------------------------------------- 1 | author = $author; 42 | return $this; 43 | } 44 | 45 | public function withFooter(FooterEmbed $footer): self 46 | { 47 | $this->footer = $footer; 48 | return $this; 49 | } 50 | 51 | public function withImage(ImageEmbed $image): self 52 | { 53 | $this->image = $image; 54 | return $this; 55 | } 56 | 57 | public function withThumbnail(ThumbnailEmbed $thumbnail): self 58 | { 59 | $this->thumbnail = $thumbnail; 60 | return $this; 61 | } 62 | 63 | public function withVideo(VideoEmbed $video): self 64 | { 65 | $this->video = $video; 66 | return $this; 67 | } 68 | 69 | public function withProvider(ProviderEmbed $provider): self 70 | { 71 | $this->provider = $provider; 72 | return $this; 73 | } 74 | 75 | public function addField(FieldEmbed $field): self 76 | { 77 | $this->fields[] = $field; 78 | return $this; 79 | } 80 | 81 | public function getType(): string 82 | { 83 | return static::TYPE_RICH; 84 | } 85 | 86 | public function toArray(): array 87 | { 88 | return $this->toMergedArray([ 89 | 'footer' => $this->footer?->toArray(), 90 | 'image' => $this->image?->toArray(), 91 | 'thumbnail' => $this->thumbnail?->toArray(), 92 | 'video' => $this->video?->toArray(), 93 | 'provider' => $this->provider?->toArray(), 94 | 'author' => $this->author?->toArray(), 95 | 'fields' => array_map(function (FieldEmbed $field): array { 96 | return $field->toArray()['field']; 97 | }, $this->fields), 98 | ]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Support/Embeds/ImageEmbed.php: -------------------------------------------------------------------------------- 1 | url = $url; 29 | } 30 | 31 | /** 32 | * A proxied url of the image 33 | * 34 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure 35 | * @param string $proxyUrl 36 | * @return $this 37 | */ 38 | public function withProxyUrl(string $proxyUrl): self 39 | { 40 | $this->proxyUrl = $proxyUrl; 41 | return $this; 42 | } 43 | 44 | /** 45 | * Image height 46 | * 47 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure 48 | * @param int $height 49 | * @return $this 50 | */ 51 | public function withHeight(int $height): self 52 | { 53 | $this->height = $height; 54 | return $this; 55 | } 56 | 57 | /** 58 | * Image width 59 | * 60 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure 61 | * @param int $width 62 | * @return $this 63 | */ 64 | public function withWidth(int $width): self 65 | { 66 | $this->width = $width; 67 | return $this; 68 | } 69 | 70 | public function getType(): string 71 | { 72 | return static::TYPE_IMAGE; 73 | } 74 | 75 | public function toArray(): array 76 | { 77 | return $this->toMergedArray([ 78 | 'image' => [ 79 | 'url' => $this->url, 80 | 'proxy_url' => $this->proxyUrl, 81 | 'height' => $this->height, 82 | 'width' => $this->width, 83 | ], 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Support/Embeds/ProviderEmbed.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | return $this; 37 | } 38 | 39 | /** 40 | * URL of provider 41 | * 42 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure 43 | * @param string $url 44 | * @return $this 45 | */ 46 | public function withUrl(string $url): self 47 | { 48 | $this->url = $url; 49 | return $this; 50 | } 51 | 52 | public function getType(): string 53 | { 54 | return static::TYPE_PROVIDER; 55 | } 56 | 57 | public function toArray(): array 58 | { 59 | return $this->toMergedArray([ 60 | 'provider' => [ 61 | 'name' => $this->name, 62 | 'url' => $this->url, 63 | ], 64 | ]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Support/Embeds/ThumbnailEmbed.php: -------------------------------------------------------------------------------- 1 | url = $url; 29 | } 30 | 31 | /** 32 | * A proxied url of the video 33 | * 34 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure 35 | * @param string $proxyUrl 36 | * @return $this 37 | */ 38 | public function withProxyUrl(string $proxyUrl): self 39 | { 40 | $this->proxyUrl = $proxyUrl; 41 | return $this; 42 | } 43 | 44 | /** 45 | * Height of video 46 | * 47 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure 48 | * @param int $height 49 | * @return $this 50 | */ 51 | public function withHeight(int $height): self 52 | { 53 | $this->height = $height; 54 | return $this; 55 | } 56 | 57 | /** 58 | * Width of video 59 | * 60 | * @see https://discord.com/developers/docs/resources/channel#embed-object-embed-video-structure 61 | * @param int $width 62 | * @return $this 63 | */ 64 | public function withWidth(int $width): self 65 | { 66 | $this->width = $width; 67 | return $this; 68 | } 69 | 70 | public function getType(): string 71 | { 72 | return static::TYPE_VIDEO; 73 | } 74 | 75 | public function toArray(): array 76 | { 77 | return $this->toMergedArray([ 78 | 'video' => [ 79 | 'url' => $this->url, 80 | 'proxy_url' => $this->proxyUrl, 81 | 'height' => $this->height, 82 | 'width' => $this->width, 83 | ], 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Support/Interactions/DiscordInteractionResponse.php: -------------------------------------------------------------------------------- 1 | status = $status; 19 | $this->type = $type; 20 | $this->data = $data; 21 | } 22 | 23 | public function getStatus(): int 24 | { 25 | return $this->status; 26 | } 27 | 28 | public function toArray(): array 29 | { 30 | return array_filter([ 31 | 'type' => $this->type, 32 | 'data' => $this->data, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Support/Interactions/Handlers/ApplicationCommandHandler.php: -------------------------------------------------------------------------------- 1 | defaultBehavior = $defaultBehavior; 24 | $this->dispatcher = $dispatcher; 25 | $this->laravel = $laravel; 26 | } 27 | 28 | public function handle(Request $request): DiscordInteractionResponse 29 | { 30 | if ($response = $this->shouldHandleEventExternally($request)) { 31 | return $response; 32 | } 33 | 34 | switch ($this->defaultBehavior) { 35 | case static::BEHAVIOR_LOAD: 36 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE); 37 | case static::BEHAVIOR_DEFER: 38 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_UPDATE_MESSAGE); 39 | } 40 | 41 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_UPDATE_MESSAGE); 42 | } 43 | 44 | protected function shouldHandleEventExternally(Request $request): ?DiscordInteractionResponse 45 | { 46 | return $this->generateResponse( 47 | new ApplicationCommandInteractionEvent($request->json()), 48 | ApplicationCommandInteractionEventListenerContract::class, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Support/Interactions/Handlers/MessageComponentInteractionHandler.php: -------------------------------------------------------------------------------- 1 | defaultBehavior = $defaultBehavior; 25 | $this->dispatcher = $dispatcher; 26 | $this->laravel = $laravel; 27 | } 28 | 29 | public function handle(Request $request): DiscordInteractionResponse 30 | { 31 | if ($response = $this->shouldHandleEventExternally($request)) { 32 | return $response; 33 | } 34 | 35 | switch ($this->defaultBehavior) { 36 | case static::BEHAVIOR_LOAD: 37 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE); 38 | case static::BEHAVIOR_DEFER: 39 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_UPDATE_MESSAGE); 40 | } 41 | 42 | return new DiscordInteractionResponse(static::RESPONSE_TYPE_DEFERRED_UPDATE_MESSAGE); 43 | } 44 | 45 | protected function shouldHandleEventExternally(Request $request): ?DiscordInteractionResponse 46 | { 47 | return $this->generateResponse( 48 | new MessageComponentInteractionEvent($request->json()), 49 | MessageComponentInteractionEventListenerContract::class, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Support/Interactions/Handlers/PingHandler.php: -------------------------------------------------------------------------------- 1 | repliedUser = $mention; 39 | return $this; 40 | } 41 | 42 | /** 43 | * Allows roles to be mentioned in message 44 | * 45 | * @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types 46 | * @return $this 47 | */ 48 | public function allowRolesMention(): self 49 | { 50 | if (!in_array(static::MENTIONS_ROLES, $this->parse)) { 51 | $this->parse[] = static::MENTIONS_ROLES; 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * Allows users to be mentioned in message 59 | * 60 | * @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types 61 | * @return $this 62 | */ 63 | public function allowUsersMention(): self 64 | { 65 | if (!in_array(static::MENTIONS_USERS, $this->parse)) { 66 | $this->parse[] = static::MENTIONS_USERS; 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Allows everyone to be mentioned in message 74 | * 75 | * @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types 76 | * @return $this 77 | */ 78 | public function allowEveryoneMention(): self 79 | { 80 | if (!in_array(static::MENTIONS_EVERYONE, $this->parse)) { 81 | $this->parse[] = static::MENTIONS_EVERYONE; 82 | } 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * Allows mentions of specific roles 89 | * 90 | * @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mentions-structure 91 | * @param array $roles 92 | * @return $this 93 | */ 94 | public function allowMentionsForRoles(array $roles): self 95 | { 96 | $this->roles = $roles; 97 | return $this; 98 | } 99 | 100 | /** 101 | * Allows mentions of specific users 102 | * 103 | * @see https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mentions-structure 104 | * @param array $users 105 | * @return $this 106 | */ 107 | public function allowMentionsForUsers(array $users): self 108 | { 109 | $this->users = $users; 110 | return $this; 111 | } 112 | 113 | public function toArray(): array 114 | { 115 | return $this->toMergedArray([ 116 | 'parse' => $this->parse, 117 | 'roles' => $this->roles, 118 | 'users' => $this->users, 119 | 'replied_user' => $this->repliedUser, 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Support/Objects/EmojiObject.php: -------------------------------------------------------------------------------- 1 | name = $name; 22 | } 23 | 24 | /** 25 | * Returns a Discord-API compliant emoji object array 26 | * 27 | * @see https://discord.com/developers/docs/resources/emoji#emoji-object-emoji-structure 28 | * @return array 29 | */ 30 | public function toArray(): array 31 | { 32 | return $this->toMergedArray([ 33 | 'name' => $this->name, 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Support/Objects/SelectOptionObject.php: -------------------------------------------------------------------------------- 1 | label = $label; 29 | $this->value = $value; 30 | } 31 | 32 | /** 33 | * An additional description of the option, max 100 characters 34 | * 35 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure 36 | * @param string $description 37 | * @return $this 38 | */ 39 | public function withDescription(string $description): self 40 | { 41 | $this->description = $description; 42 | return $this; 43 | } 44 | 45 | /** 46 | * Will render this option as selected by default 47 | * 48 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure 49 | * @param bool $default 50 | * @return $this 51 | */ 52 | public function default(bool $default = true): self 53 | { 54 | $this->default = $default; 55 | return $this; 56 | } 57 | 58 | /** 59 | * Returns a Discord-API compliant select option array 60 | * 61 | * @see https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure 62 | * @return array 63 | */ 64 | public function toArray(): array 65 | { 66 | return $this->toMergedArray($this->mergeEmojiObject([ 67 | 'label' => $this->label, 68 | 'value' => $this->value, 69 | 'description' => $this->description, 70 | 'default' => $this->default, 71 | ])); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Support/SupportObject.php: -------------------------------------------------------------------------------- 1 | arrayFilterRecursive([]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/Traits/ApplicationCommand/HasAutoComplete.php: -------------------------------------------------------------------------------- 1 | autocomplete = $autocomplete; 13 | return $this; 14 | } 15 | 16 | protected function mergeAutocomplete(array $merge): array 17 | { 18 | return array_merge($merge, array_filter([ 19 | 'autocomplete' => $this->autocomplete, 20 | ])); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Support/Traits/ApplicationCommand/HasChoices.php: -------------------------------------------------------------------------------- 1 | choices)) { 18 | $this->choices = []; 19 | } 20 | 21 | $this->choices[] = $choice; 22 | return $this; 23 | } 24 | 25 | public function choices(array $choices): self 26 | { 27 | $this->choices = $choices; 28 | return $this; 29 | } 30 | 31 | protected function mergeChoices(array $merge): array 32 | { 33 | if (empty($this->choices)) { 34 | return $merge; 35 | } 36 | 37 | return array_merge($merge, [ 38 | 'choices' => array_map(function (OptionChoice $choice): array { 39 | return $this->choiceTransformer($choice); 40 | }, $this->choices) 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Support/Traits/ApplicationCommand/HasOptions.php: -------------------------------------------------------------------------------- 1 | options)) { 18 | $this->options = []; 19 | } 20 | 21 | $this->options[] = $option; 22 | return $this; 23 | } 24 | 25 | protected function mergeOptions(array $merge = []): array 26 | { 27 | if (empty($this->options)) { 28 | return $merge; 29 | } 30 | 31 | return array_merge($merge, array_filter([ 32 | 'options' => array_map(function (CommandOption $option): array { 33 | return $option->toArray(); 34 | }, $this->options), 35 | ])); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Support/Traits/ApplicationCommand/NoChoiceTransformer.php: -------------------------------------------------------------------------------- 1 | toArray(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Support/Traits/DiscordApiService.php: -------------------------------------------------------------------------------- 1 | apiUrl, $endpoint); 20 | 21 | return $this->httpClient->request($method, $url, [ 22 | 'headers' => [ 23 | 'Authorization' => sprintf('Bot %s', $this->token), 24 | ], 25 | 'json' => $payload, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/Traits/FiltersRecursive.php: -------------------------------------------------------------------------------- 1 | $value) { 11 | if (is_array($value)) { 12 | $toFilter[$key] = $this->arrayFilterRecursive($value); 13 | } 14 | } 15 | 16 | return array_filter($toFilter); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Support/Traits/HasEmojiObject.php: -------------------------------------------------------------------------------- 1 | emoji = $emoji; 15 | return $this; 16 | } 17 | 18 | protected function mergeEmojiObject(array $mergeWith): array 19 | { 20 | return array_merge($mergeWith, [ 21 | 'emoji' => ($this->emoji) ? $this->emoji->toArray() : null, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Support/Traits/HasInteractionListeners.php: -------------------------------------------------------------------------------- 1 | getStaticVariables(); 23 | return $this->laravel->make($attributes['listener']); 24 | } 25 | 26 | protected function getListenersFor(string $eventClass): array 27 | { 28 | $listeners = $this->dispatcher->getListeners($eventClass); 29 | return array_values(array_map(function (\Closure $listener) { 30 | return $this->makeListenerFromClosure($listener); 31 | }, $listeners)); 32 | } 33 | 34 | protected function defaultBehaviorResponse(array $listeners, $event): DiscordInteractionResponse 35 | { 36 | $listener = $listeners[0]; 37 | 38 | $replyContent = $listener->replyContent($event); 39 | $behavior = $listener->behavior($event); 40 | 41 | $data = null; 42 | if (!empty($replyContent)) { 43 | $data = [ 44 | 'content' => $replyContent, 45 | ]; 46 | } 47 | 48 | return new DiscordInteractionResponse($behavior, $data); 49 | } 50 | 51 | /** 52 | * Determines if an incoming interaction should be handled by the user's application or by this package directly. 53 | * 54 | * If there are any listeners subscribed to the Message- or App- ComponentInteractionEvent, those should be dispatched. 55 | * Additionally, if there is a listener implementing the contract Message- or App- ComponentInteractionEventListenerContract 56 | * then that listener should be instantiated and have certain methods called to generate a DiscordInteractionResponse. 57 | * 58 | * This essentially facilitates overriding the default behavior of responding to interaction requests. 59 | * 60 | * @return DiscordInteractionResponse|null 61 | */ 62 | protected function generateResponse(AbstractInteractionEvent $event, string $listenerClass): ?DiscordInteractionResponse 63 | { 64 | $listeners = $this->getListenersFor(get_class($event)); 65 | $listenersImplementingInterface = array_values(array_filter($listeners, function ($listener) use ($listenerClass): bool { 66 | return $listener instanceof $listenerClass; 67 | })); 68 | 69 | if (!empty($listeners)) { 70 | $this->dispatcher->dispatch($event); 71 | } 72 | 73 | if (empty($listenersImplementingInterface)) { 74 | return null; 75 | } 76 | 77 | return $this->defaultBehaviorResponse($listenersImplementingInterface, $event); 78 | } 79 | 80 | protected abstract function shouldHandleEventExternally(Request $request): ?DiscordInteractionResponse; 81 | } 82 | -------------------------------------------------------------------------------- /src/Support/Traits/MergesArrays.php: -------------------------------------------------------------------------------- 1 | arrayFilterRecursive(array_merge(parent::toArray(), $toMerge)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | addToAssertionCount($container->mockery_getExpectationCount()); 15 | } 16 | 17 | \Mockery::close(); 18 | } 19 | 20 | parent::tearDown(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Traits/BasicCommandOptionTests.php: -------------------------------------------------------------------------------- 1 | assertSame($this->expectedType, $this->option->getType()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Traits/BasicCommandTests.php: -------------------------------------------------------------------------------- 1 | assertSame($this->expectedType, $this->command->getType()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Unit/Channels/DiscordNotificationChannelTest.php: -------------------------------------------------------------------------------- 1 | discordApiService = \Mockery::mock(DiscordApiServiceContract::class); 26 | $this->channel = new DiscordNotificationChannel($this->discordApiService); 27 | } 28 | 29 | public function testSendPlainText() 30 | { 31 | $notifiable = \Mockery::mock(Notifiable::class); 32 | $notification = \Mockery::mock(DiscordNotificationContract::class); 33 | 34 | $discordNotificationArray = [ 35 | 'contentType' => 'plain', 36 | 'channelId' => '12345', 37 | 'message' => 'test message', 38 | ]; 39 | 40 | $expectedResponse = [ 41 | 'key' => 'value', 42 | ]; 43 | 44 | $notification->shouldReceive('toDiscord') 45 | ->once() 46 | ->with($notifiable) 47 | ->andReturn($discordNotificationArray); 48 | 49 | $this->discordApiService->shouldReceive('sendTextMessage') 50 | ->once() 51 | ->with('12345', 'test message', []) 52 | ->andReturn($expectedResponse); 53 | 54 | $result = $this->channel->send($notifiable, $notification); 55 | $this->assertEquals($expectedResponse, $result); 56 | } 57 | 58 | public function testSendRichText() 59 | { 60 | $notifiable = \Mockery::mock(Notifiable::class); 61 | $notification = \Mockery::mock(DiscordNotificationContract::class); 62 | 63 | $embed1 = \Mockery::mock(Embed::class); 64 | $embed2 = \Mockery::mock(Embed::class); 65 | 66 | $component1 = \Mockery::mock(Component::class); 67 | $component2 = \Mockery::mock(Component::class); 68 | 69 | $discordNotificationArray = [ 70 | 'contentType' => 'rich', 71 | 'channelId' => '12345', 72 | 'embeds' => [$embed1, $embed2], 73 | 'components' => [$component1, $component2], 74 | ]; 75 | 76 | $expectedResponse = [ 77 | 'key' => 'value', 78 | ]; 79 | 80 | $notification->shouldReceive('toDiscord') 81 | ->once() 82 | ->with($notifiable) 83 | ->andReturn($discordNotificationArray); 84 | 85 | $this->discordApiService->shouldReceive('sendRichTextMessage') 86 | ->once() 87 | ->with('12345', [$embed1, $embed2], [$component1, $component2], []) 88 | ->andReturn($expectedResponse); 89 | 90 | $result = $this->channel->send($notifiable, $notification); 91 | $this->assertEquals($expectedResponse, $result); 92 | } 93 | 94 | public function testSendWithInvalidContentTypeThrowsException() 95 | { 96 | $notifiable = \Mockery::mock(Notifiable::class); 97 | $notification = \Mockery::mock(DiscordNotificationContract::class); 98 | 99 | $discordNotificationArray = [ 100 | 'contentType' => 'invalid', 101 | ]; 102 | 103 | $notification->shouldReceive('toDiscord') 104 | ->once() 105 | ->with($notifiable) 106 | ->andReturn($discordNotificationArray); 107 | 108 | $this->expectException(\InvalidArgumentException::class); 109 | $this->expectExceptionMessage('invalid is not a valid contentType'); 110 | 111 | $this->channel->send($notifiable, $notification); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/Unit/Events/ApplicationCommandInteractionEventTest.php: -------------------------------------------------------------------------------- 1 | assertSame($parameterBag, $event->getInteractionRequest()); 19 | } 20 | 21 | public function testGetters() 22 | { 23 | $commandName = 'test-command-name'; 24 | $commandId = 'test-command-id'; 25 | $channelId = 'test-channel-id'; 26 | $applicationId = 'test-app-id'; 27 | $type = 42; 28 | 29 | $data = [ 30 | 'name' => $commandName, 31 | 'id' => $commandId, 32 | 'type' => $type, 33 | ]; 34 | 35 | $parameterBag = new ParameterBag([ 36 | 'application_id' => $applicationId, 37 | 'channel_id' => $channelId, 38 | 'data' => $data, 39 | ]); 40 | 41 | $event = new ApplicationCommandInteractionEvent($parameterBag); 42 | 43 | $this->assertSame($commandName, $event->getCommandName()); 44 | $this->assertSame($commandId, $event->getCommandId()); 45 | $this->assertSame($channelId, $event->getChannelId()); 46 | $this->assertSame($applicationId, $event->getApplicationId()); 47 | $this->assertSame($type, $event->getCommandType()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Unit/Events/MessageComponentInteractionEventTest.php: -------------------------------------------------------------------------------- 1 | assertSame($parameterBag, $event->getInteractionRequest()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Unit/Services/DiscordApiServiceTest.php: -------------------------------------------------------------------------------- 1 | httpClient = \Mockery::mock(ClientInterface::class); 29 | $this->service = new DiscordApiService($this->token, $this->apiUrl, $this->httpClient); 30 | } 31 | 32 | public function testSendTextMessage() 33 | { 34 | $channelId = '12345'; 35 | $message = 'test message'; 36 | 37 | $expectedRequestPayload = [ 38 | 'content' => $message, 39 | ]; 40 | 41 | $expectedResponse = [ 42 | 'id' => '54321', 43 | ]; 44 | 45 | $responseMock = \Mockery::mock(ResponseInterface::class); 46 | $responseMock->shouldAllowMockingMethod('getContents'); 47 | 48 | $responseMock->shouldReceive('getBody')->once()->andReturnSelf(); 49 | $responseMock->shouldReceive('getContents')->once()->andReturn(json_encode($expectedResponse)); 50 | 51 | $this->httpClient->shouldReceive('request') 52 | ->once() 53 | ->with('POST', sprintf('%s/channels/%s/messages', $this->apiUrl, $channelId), [ 54 | 'headers' => [ 55 | 'Authorization' => 'Bot ' . $this->token, 56 | ], 57 | 'json' => $expectedRequestPayload, 58 | ]) 59 | ->andReturn($responseMock); 60 | 61 | $result = $this->service->sendTextMessage($channelId, $message); 62 | $this->assertEquals($expectedResponse, $result); 63 | } 64 | 65 | public function testSendRichTextMessage() 66 | { 67 | $channelId = '12345'; 68 | 69 | $expectedEmbed1Array = ['k1' => 'v1']; 70 | $expectedEmbed2Array = ['k2' => 'v2']; 71 | 72 | $expectedComponent1Array = ['k3' => 'v3']; 73 | $expectedComponent2Array = ['k4' => 'v4']; 74 | 75 | $embed1 = \Mockery::mock(Embed::class); 76 | $embed2 = \Mockery::mock(Embed::class); 77 | 78 | $component1 = \Mockery::mock(Component::class); 79 | $component2 = \Mockery::mock(Component::class); 80 | 81 | $embed1->shouldReceive('toArray')->andReturn($expectedEmbed1Array); 82 | $embed2->shouldReceive('toArray')->andReturn($expectedEmbed2Array); 83 | $component1->shouldReceive('toArray')->andReturn($expectedComponent1Array); 84 | $component2->shouldReceive('toArray')->andReturn($expectedComponent2Array); 85 | 86 | $expectedRequestPayload = [ 87 | 'embeds' => [$expectedEmbed1Array, $expectedEmbed2Array], 88 | 'components' => [$expectedComponent1Array, $expectedComponent2Array], 89 | ]; 90 | 91 | $expectedResponse = [ 92 | 'id' => '54321', 93 | ]; 94 | 95 | $responseMock = \Mockery::mock(ResponseInterface::class); 96 | $responseMock->shouldAllowMockingMethod('getContents'); 97 | 98 | $responseMock->shouldReceive('getBody')->andReturnSelf(); 99 | $responseMock->shouldReceive('getContents')->andReturn(json_encode($expectedResponse)); 100 | 101 | $this->httpClient->shouldReceive('request') 102 | ->once() 103 | ->with('POST', sprintf('%s/channels/%s/messages', $this->apiUrl, $channelId), [ 104 | 'headers' => [ 105 | 'Authorization' => 'Bot ' . $this->token, 106 | ], 107 | 'json' => $expectedRequestPayload, 108 | ]) 109 | ->andReturn($responseMock); 110 | 111 | $result = $this->service->sendRichTextMessage($channelId, [$embed1, $embed2], [$component1, $component2]); 112 | $this->assertEquals($expectedResponse, $result); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/Unit/Services/DiscordInteractionServiceTest.php: -------------------------------------------------------------------------------- 1 | publicKey = sodium_crypto_sign_publickey($keypair); 37 | $this->privateKey = sodium_crypto_sign_secretkey($keypair); 38 | 39 | $this->laravel = \Mockery::mock(Application::class); 40 | 41 | $this->service = new DiscordInteractionService($this->applicationId, bin2hex($this->publicKey), $this->laravel); 42 | } 43 | 44 | public function testHandleInteractionRequestThrows404WhenNoHandlerFound() 45 | { 46 | $timestamp = '12345'; 47 | $body = 'test-body'; 48 | $signature = bin2hex(sodium_crypto_sign_detached(sprintf('%s%s', $timestamp, $body), $this->privateKey)); 49 | 50 | $expectedJson = [ 51 | 'type' => 999, // invalid type 52 | ]; 53 | 54 | $request = \Mockery::mock(Request::class); 55 | $request->shouldAllowMockingMethod('all'); 56 | 57 | $request->shouldReceive('json')->once()->andReturnSelf(); 58 | $request->shouldReceive('all')->once()->andReturn($expectedJson); 59 | 60 | $request->shouldReceive('header')->once()->with('X-Signature-Ed25519')->andReturn($signature); 61 | $request->shouldReceive('header')->once()->with('X-Signature-Timestamp')->andReturn($timestamp); 62 | $request->shouldReceive('getContent')->once()->andReturn($body); 63 | 64 | $this->expectException(NotFoundHttpException::class); 65 | $this->service->handleInteractionRequest($request); 66 | } 67 | 68 | public function testHandleInteractionRequestThrows401WhenMissingRequiredData() 69 | { 70 | $request = \Mockery::mock(Request::class); 71 | $request->shouldAllowMockingMethod('all'); 72 | 73 | $request->shouldReceive('header')->once()->with('X-Signature-Ed25519')->andReturnNull(); 74 | $request->shouldReceive('header')->once()->with('X-Signature-Timestamp')->andReturnNull(); 75 | $request->shouldReceive('getContent')->once()->andReturnNull(); 76 | 77 | $this->expectException(UnauthorizedHttpException::class); 78 | $this->service->handleInteractionRequest($request); 79 | } 80 | 81 | public function testHandleInteractionRequestThrows401WhenSignatureVerificationFails() 82 | { 83 | $timestamp = '12345'; 84 | $body = 'test-body'; 85 | $signature = bin2hex(sodium_crypto_sign_detached(sprintf('%s%s', $timestamp, $body), $this->privateKey)); 86 | 87 | $request = \Mockery::mock(Request::class); 88 | $request->shouldAllowMockingMethod('all'); 89 | 90 | $request->shouldReceive('header')->once()->with('X-Signature-Ed25519')->andReturn($signature); 91 | $request->shouldReceive('header')->once()->with('X-Signature-Timestamp')->andReturn($timestamp); 92 | $request->shouldReceive('getContent')->once()->andReturn('something different'); 93 | 94 | $this->expectException(UnauthorizedHttpException::class); 95 | $this->service->handleInteractionRequest($request); 96 | } 97 | 98 | public function testHandleInteractionRequestThrows401OnSodiumException() 99 | { 100 | $timestamp = '12345'; 101 | $body = 'test-body'; 102 | $signature = bin2hex(sodium_crypto_sign_detached(sprintf('%s%s', $timestamp, $body), $this->privateKey)); 103 | 104 | $request = \Mockery::mock(Request::class); 105 | $request->shouldAllowMockingMethod('all'); 106 | 107 | $request->shouldReceive('header')->once()->with('X-Signature-Ed25519')->andReturn($signature); 108 | $request->shouldReceive('header')->once()->with('X-Signature-Timestamp')->andReturn($timestamp); 109 | $request->shouldReceive('getContent')->once()->andReturn('something different'); 110 | 111 | $this->expectException(UnauthorizedHttpException::class); 112 | 113 | $service = new DiscordInteractionService($this->applicationId, 'abc123', $this->laravel); 114 | $service->handleInteractionRequest($request); 115 | } 116 | 117 | /** 118 | * @dataProvider handlerDataProvider 119 | */ 120 | public function testHandleInteractionBuildsAndHandlesRequest(int $type, string $expectedHandlerClass) 121 | { 122 | $timestamp = '12345'; 123 | $body = 'test-body'; 124 | $signature = bin2hex(sodium_crypto_sign_detached(sprintf('%s%s', $timestamp, $body), $this->privateKey)); 125 | 126 | $expectedJson = [ 127 | 'type' => $type, 128 | ]; 129 | 130 | $request = \Mockery::mock(Request::class); 131 | $request->shouldAllowMockingMethod('all'); 132 | 133 | $request->shouldReceive('json')->once()->andReturnSelf(); 134 | $request->shouldReceive('all')->once()->andReturn($expectedJson); 135 | 136 | $request->shouldReceive('header')->once()->with('X-Signature-Ed25519')->andReturn($signature); 137 | $request->shouldReceive('header')->once()->with('X-Signature-Timestamp')->andReturn($timestamp); 138 | $request->shouldReceive('getContent')->once()->andReturn($body); 139 | 140 | $expectedHandlerReturn = new DiscordInteractionResponse(0); 141 | 142 | $handlerMock = \Mockery::mock(InteractionHandler::class); 143 | $handlerMock->shouldReceive('handle')->once()->with($request)->andReturn($expectedHandlerReturn); 144 | 145 | $this->laravel->shouldReceive('make')->once()->with($expectedHandlerClass)->andReturn($handlerMock); 146 | 147 | $result = $this->service->handleInteractionRequest($request); 148 | $this->assertSame($expectedHandlerReturn, $result); 149 | } 150 | 151 | public function handlerDataProvider(): array 152 | { 153 | return [ 154 | [1, PingHandler::class], 155 | [2, ApplicationCommandHandler::class], 156 | [3, MessageComponentInteractionHandler::class], 157 | ]; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/Unit/Support/Builder/ComponentBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'v1']; 21 | $expectedComponent2Array = ['k2' => 'v2']; 22 | 23 | $component1 = \Mockery::mock(Component::class); 24 | $component2 = \Mockery::mock(Component::class); 25 | 26 | $component1->shouldReceive('toArray')->andReturn($expectedComponent1Array); 27 | $component2->shouldReceive('toArray')->andReturn($expectedComponent2Array); 28 | 29 | $builder->addComponent($component1)->addComponent($component2); 30 | 31 | $actionRow = $builder->getActionRow(); 32 | 33 | $this->assertInstanceOf(ActionRow::class, $actionRow); 34 | $this->assertEquals([ 35 | 'type' => Component::TYPE_ACTION_ROW, 36 | 'components' => [$expectedComponent1Array, $expectedComponent2Array], 37 | ], $actionRow->toArray()); 38 | } 39 | 40 | public function testWithSelectMenuObject() 41 | { 42 | $builder = new ComponentBuilder(); 43 | 44 | $label = 'test label'; 45 | $value = 'test value'; 46 | 47 | $option = $builder->withSelectOptionObject($label, $value); 48 | 49 | $this->assertInstanceOf(SelectOptionObject::class, $option); 50 | $this->assertEquals([ 51 | 'label' => $label, 52 | 'value' => $value, 53 | ], $option->toArray()); 54 | } 55 | 56 | public function testAddActionButton() 57 | { 58 | $builder = new ComponentBuilder(); 59 | 60 | $label = 'test label'; 61 | $customId = 'custom-id'; 62 | 63 | $builder->addActionButton($label, $customId); 64 | $actionRow = $builder->getActionRow(); 65 | 66 | $this->assertEquals([ 67 | 'type' => Component::TYPE_ACTION_ROW, 68 | 'components' => [ 69 | [ 70 | 'type' => Component::TYPE_BUTTON, 71 | 'custom_id' => $customId, 72 | 'style' => GenericButtonComponent::STYLE_PRIMARY, 73 | 'label' => $label, 74 | ], 75 | ], 76 | ], $actionRow->toArray()); 77 | } 78 | 79 | public function testAddLinkButton() 80 | { 81 | $builder = new ComponentBuilder(); 82 | 83 | $label = 'test label'; 84 | $url = 'test url'; 85 | 86 | $builder->addLinkButton($label, $url); 87 | $actionRow = $builder->getActionRow(); 88 | 89 | $this->assertEquals([ 90 | 'type' => Component::TYPE_ACTION_ROW, 91 | 'components' => [ 92 | [ 93 | 'type' => Component::TYPE_BUTTON, 94 | 'url' => $url, 95 | 'style' => GenericButtonComponent::STYLE_LINK, 96 | 'label' => $label, 97 | ], 98 | ], 99 | ], $actionRow->toArray()); 100 | } 101 | 102 | public function testAddSelectMenuComponent() 103 | { 104 | $builder = new ComponentBuilder(); 105 | 106 | $customId = 'custom-id'; 107 | $builder->addSelectMenuComponent([ 108 | $builder->withSelectOptionObject('option1', 'value1'), 109 | $builder->withSelectOptionObject('option2', 'value2'), 110 | ], $customId); 111 | 112 | $actionRow = $builder->getActionRow(); 113 | $this->assertEquals([ 114 | 'type' => Component::TYPE_ACTION_ROW, 115 | 'components' => [ 116 | [ 117 | 'type' => Component::TYPE_SELECT_MENU, 118 | 'custom_id' => $customId, 119 | 'options' => [ 120 | [ 121 | 'label' => 'option1', 122 | 'value' => 'value1', 123 | ], 124 | [ 125 | 'label' => 'option2', 126 | 'value' => 'value2', 127 | ] 128 | ], 129 | ], 130 | ], 131 | ], $actionRow->toArray()); 132 | } 133 | 134 | public function testAddShortTextInput() 135 | { 136 | $builder = new ComponentBuilder(); 137 | 138 | $label = 'test label'; 139 | $customId = 'custom-id'; 140 | 141 | $builder->addShortTextInput($label, $customId); 142 | $actionButton = $builder->getActionRow(); 143 | 144 | $this->assertEquals([ 145 | 'type' => Component::TYPE_ACTION_ROW, 146 | 'components' => [ 147 | [ 148 | 'type' => Component::TYPE_TEXT_INPUT, 149 | 'custom_id' => $customId, 150 | 'style' => GenericTextInputComponent::STYLE_SHORT, 151 | 'label' => $label, 152 | ], 153 | ], 154 | ], $actionButton->toArray()); 155 | } 156 | 157 | public function testParagraphTextInput() 158 | { 159 | $builder = new ComponentBuilder(); 160 | 161 | $label = 'test label'; 162 | $customId = 'custom-id'; 163 | 164 | $builder->addParagraphTextInput($label, $customId); 165 | $actionButton = $builder->getActionRow(); 166 | 167 | $this->assertEquals([ 168 | 'type' => Component::TYPE_ACTION_ROW, 169 | 'components' => [ 170 | [ 171 | 'type' => Component::TYPE_TEXT_INPUT, 172 | 'custom_id' => $customId, 173 | 'style' => GenericTextInputComponent::STYLE_PARAGRAPH, 174 | 'label' => $label, 175 | ], 176 | ], 177 | ], $actionButton->toArray()); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /tests/Unit/Support/Builder/EmbedBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'v1']; 17 | $expectedEmbed2Array = ['k2' => 'v2']; 18 | 19 | $embed1 = \Mockery::mock(Embed::class); 20 | $embed2 = \Mockery::mock(Embed::class); 21 | 22 | $embed1->shouldReceive('toArray')->andReturn($expectedEmbed1Array); 23 | $embed2->shouldReceive('toArray')->andReturn($expectedEmbed2Array); 24 | 25 | $builder->addEmbed($embed1)->addEmbed($embed2); 26 | 27 | $this->assertSame([$embed1, $embed2], $builder->getEmbeds()); 28 | $this->assertEquals([ 29 | $expectedEmbed1Array, 30 | $expectedEmbed2Array, 31 | ], $builder->toArray()); 32 | } 33 | 34 | public function testAddFooter() 35 | { 36 | $footerText = 'test text'; 37 | 38 | $builder = new EmbedBuilder(); 39 | $builder->addFooter($footerText); 40 | 41 | $this->assertEquals([ 42 | [ 43 | 'type' => Embed::TYPE_FOOTER, 44 | 'footer' => [ 45 | 'text' => $footerText, 46 | ], 47 | ] 48 | ], $builder->toArray()); 49 | } 50 | 51 | public function testAddImage() 52 | { 53 | $imageUrl = 'test url'; 54 | 55 | $builder = new EmbedBuilder(); 56 | $builder->addImage($imageUrl); 57 | 58 | $this->assertEquals([ 59 | [ 60 | 'type' => Embed::TYPE_IMAGE, 61 | 'image' => [ 62 | 'url' => $imageUrl, 63 | ], 64 | ] 65 | ], $builder->toArray()); 66 | } 67 | 68 | public function testAddThumbnail() 69 | { 70 | $imageUrl = 'test url'; 71 | 72 | $builder = new EmbedBuilder(); 73 | $builder->addThumbnail($imageUrl); 74 | 75 | $this->assertEquals([ 76 | [ 77 | 'type' => Embed::TYPE_THUMBNAIL, 78 | 'image' => [ 79 | 'url' => $imageUrl, 80 | ], 81 | ] 82 | ], $builder->toArray()); 83 | } 84 | 85 | public function testAddVideo() 86 | { 87 | $videoUrl = 'test url'; 88 | 89 | $builder = new EmbedBuilder(); 90 | $builder->addVideo($videoUrl); 91 | 92 | $this->assertEquals([ 93 | [ 94 | 'type' => Embed::TYPE_VIDEO, 95 | 'video' => [ 96 | 'url' => $videoUrl, 97 | ], 98 | ] 99 | ], $builder->toArray()); 100 | } 101 | 102 | public function testAddProvider() 103 | { 104 | $name = 'test name'; 105 | $url = 'test url'; 106 | 107 | $builder = new EmbedBuilder(); 108 | $builder->addProvider($name, $url); 109 | 110 | $this->assertEquals([ 111 | [ 112 | 'type' => Embed::TYPE_PROVIDER, 113 | 'provider' => [ 114 | 'name' => $name, 115 | 'url' => $url, 116 | ], 117 | ] 118 | ], $builder->toArray()); 119 | } 120 | 121 | public function testAddAuthor() 122 | { 123 | $authorName = 'test name'; 124 | 125 | $builder = new EmbedBuilder(); 126 | $builder->addAuthor($authorName); 127 | 128 | $this->assertEquals([ 129 | [ 130 | 'type' => Embed::TYPE_AUTHOR, 131 | 'author' => [ 132 | 'name' => $authorName, 133 | ], 134 | ] 135 | ], $builder->toArray()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/MessageCommandTest.php: -------------------------------------------------------------------------------- 1 | expectedType = Command::TYPE_MESSAGE; 20 | $this->command = new MessageCommand('test'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $command = new MessageCommand($name); 27 | 28 | $result = $command->toArray(); 29 | $this->assertEquals([ 30 | 'type' => Command::TYPE_MESSAGE, 31 | 'name' => $name, 32 | ], $result); 33 | } 34 | 35 | public function testToArrayWithOptions() 36 | { 37 | $name = 'test-name'; 38 | $version = 'test-version'; 39 | $defaultMemPermissions = 'test-perm'; 40 | $parentAppId = 'test-app-id'; 41 | 42 | $nameLocal = ['en-us']; 43 | $descLocal = ['es']; 44 | 45 | $command = new MessageCommand($name); 46 | 47 | $command->version($version); 48 | $command->dmPermission(); 49 | $command->defaultPermission(); 50 | $command->defaultMemberPermissions($defaultMemPermissions); 51 | $command->nsfw(); 52 | $command->parentApplication($parentAppId); 53 | 54 | $command->nameLocalizations($nameLocal); 55 | $command->descriptionLocalizations($descLocal); 56 | 57 | $result = $command->toArray(); 58 | $this->assertEquals([ 59 | 'type' => Command::TYPE_MESSAGE, 60 | 'name' => $name, 61 | 'application_id' => $parentAppId, 62 | 'default_member_permissions' => $defaultMemPermissions, 63 | 'dm_permission' => true, 64 | 'default_permission' => true, 65 | 'nsfw' => true, 66 | 'version' => $version, 67 | 'name_localizations' => $nameLocal, 68 | 'description_localizations' => $descLocal, 69 | ], $result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/AttachmentOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_ATTACHMENT; 20 | $this->option = new AttachmentOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new AttachmentOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_ATTACHMENT, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new AttachmentOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_ATTACHMENT, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/BooleanOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_BOOLEAN; 20 | $this->option = new BooleanOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new BooleanOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_BOOLEAN, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new BooleanOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_BOOLEAN, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/ChannelOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_CHANNEL; 20 | $this->option = new ChannelOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new ChannelOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_CHANNEL, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new ChannelOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']) 47 | ->channelTypes(['test']); 48 | 49 | $this->assertEquals([ 50 | 'type' => CommandOption::TYPE_CHANNEL, 51 | 'name' => $name, 52 | 'description' => $description, 53 | 'required' => true, 54 | 'name_localizations' => ['l1'], 55 | 'description_localizations' => ['l2'], 56 | 'channel_types' => ['test'], 57 | ], $option->toArray()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/IntegerOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_INTEGER; 21 | $this->option = new IntegerOption('option', 'desc'); 22 | } 23 | 24 | public function testToArray() 25 | { 26 | $name = 'test-name'; 27 | $description = 'test-desc'; 28 | 29 | $option = new IntegerOption($name, $description); 30 | 31 | $this->assertEquals([ 32 | 'type' => CommandOption::TYPE_INTEGER, 33 | 'name' => $name, 34 | 'description' => $description, 35 | ], $option->toArray()); 36 | } 37 | 38 | public function testToArrayWithOptions() 39 | { 40 | $name = 'test-name'; 41 | $description = 'test-desc'; 42 | 43 | $option = new IntegerOption($name, $description); 44 | 45 | $option->required() 46 | ->nameLocalizations(['l1']) 47 | ->descriptionLocalizations(['l2']) 48 | ->minValue(42) 49 | ->maxValue(142); 50 | 51 | $this->assertEquals([ 52 | 'type' => CommandOption::TYPE_INTEGER, 53 | 'name' => $name, 54 | 'description' => $description, 55 | 'required' => true, 56 | 'name_localizations' => ['l1'], 57 | 'description_localizations' => ['l2'], 58 | 'min_value' => 42, 59 | 'max_value' => 142, 60 | ], $option->toArray()); 61 | } 62 | 63 | public function testToArrayWithChoices() 64 | { 65 | $name = 'test-name'; 66 | $description = 'test-desc'; 67 | 68 | $option = new IntegerOption($name, $description); 69 | 70 | $choice1 = new OptionChoice('choice1', '42'); 71 | $choice2 = new OptionChoice('choice2', '52'); 72 | $choice3 = new OptionChoice('choice3', '62'); 73 | 74 | $option->choices([$choice1, $choice2, $choice3]); 75 | 76 | $this->assertEquals([ 77 | 'type' => CommandOption::TYPE_INTEGER, 78 | 'name' => $name, 79 | 'description' => $description, 80 | 'choices' => [ 81 | [ 82 | 'name' => 'choice1', 83 | 'value' => 42, 84 | ], 85 | [ 86 | 'name' => 'choice2', 87 | 'value' => 52, 88 | ], 89 | [ 90 | 'name' => 'choice3', 91 | 'value' => 62, 92 | ], 93 | ], 94 | ], $option->toArray()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/MentionableOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_MENTIONABLE; 20 | $this->option = new MentionableOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new MentionableOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_MENTIONABLE, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new MentionableOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_MENTIONABLE, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/NumberOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_NUMBER; 22 | $this->option = new NumberOption('option', 'desc'); 23 | } 24 | 25 | public function testToArray() 26 | { 27 | $name = 'test-name'; 28 | $description = 'test-desc'; 29 | 30 | $option = new NumberOption($name, $description); 31 | 32 | $this->assertEquals([ 33 | 'type' => CommandOption::TYPE_NUMBER, 34 | 'name' => $name, 35 | 'description' => $description, 36 | ], $option->toArray()); 37 | } 38 | 39 | public function testToArrayWithOptions() 40 | { 41 | $name = 'test-name'; 42 | $description = 'test-desc'; 43 | 44 | $option = new NumberOption($name, $description); 45 | 46 | $option->required() 47 | ->nameLocalizations(['l1']) 48 | ->descriptionLocalizations(['l2']) 49 | ->minValue(4.2) 50 | ->maxValue(142); 51 | 52 | $this->assertEquals([ 53 | 'type' => CommandOption::TYPE_NUMBER, 54 | 'name' => $name, 55 | 'description' => $description, 56 | 'required' => true, 57 | 'name_localizations' => ['l1'], 58 | 'description_localizations' => ['l2'], 59 | 'min_value' => 4.2, 60 | 'max_value' => 142, 61 | ], $option->toArray()); 62 | } 63 | 64 | public function testToArrayWithChoices() 65 | { 66 | $name = 'test-name'; 67 | $description = 'test-desc'; 68 | 69 | $option = new NumberOption($name, $description); 70 | 71 | $choice1 = new OptionChoice('choice1', '4.2'); 72 | $choice2 = new OptionChoice('choice2', '52'); 73 | $choice3 = new OptionChoice('choice3', '6.2'); 74 | 75 | $option->choices([$choice1, $choice2, $choice3]); 76 | 77 | $this->assertEquals([ 78 | 'type' => CommandOption::TYPE_NUMBER, 79 | 'name' => $name, 80 | 'description' => $description, 81 | 'choices' => [ 82 | [ 83 | 'name' => 'choice1', 84 | 'value' => 4.2, 85 | ], 86 | [ 87 | 'name' => 'choice2', 88 | 'value' => 52, 89 | ], 90 | [ 91 | 'name' => 'choice3', 92 | 'value' => 6.2, 93 | ], 94 | ], 95 | ], $option->toArray()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/OptionChoiceTest.php: -------------------------------------------------------------------------------- 1 | nameLocalizations(['l1']); 16 | 17 | $this->assertEquals([ 18 | 'name' => 'test-name', 19 | 'value' => 'test-value', 20 | 'name_localizations' => ['l1'], 21 | ], $choice->toArray()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/RoleOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_ROLE; 20 | $this->option = new RoleOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new RoleOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_ROLE, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new RoleOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_ROLE, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/StringOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_STRING; 21 | $this->option = new StringOption('option', 'desc'); 22 | } 23 | 24 | public function testToArray() 25 | { 26 | $name = 'test-name'; 27 | $description = 'test-desc'; 28 | 29 | $option = new StringOption($name, $description); 30 | 31 | $this->assertEquals([ 32 | 'type' => CommandOption::TYPE_STRING, 33 | 'name' => $name, 34 | 'description' => $description, 35 | ], $option->toArray()); 36 | } 37 | 38 | public function testToArrayWithOptions() 39 | { 40 | $name = 'test-name'; 41 | $description = 'test-desc'; 42 | 43 | $option = new StringOption($name, $description); 44 | 45 | $option->required() 46 | ->nameLocalizations(['l1']) 47 | ->descriptionLocalizations(['l2']) 48 | ->maxLength(123) 49 | ->autocomplete(); 50 | 51 | $this->assertEquals([ 52 | 'type' => CommandOption::TYPE_STRING, 53 | 'name' => $name, 54 | 'description' => $description, 55 | 'required' => true, 56 | 'name_localizations' => ['l1'], 57 | 'description_localizations' => ['l2'], 58 | 'max_length' => 123, 59 | 'autocomplete' => true, 60 | ], $option->toArray()); 61 | } 62 | 63 | public function testToArrayWithChoices() 64 | { 65 | $name = 'test-name'; 66 | $description = 'test-desc'; 67 | 68 | $option = new StringOption($name, $description); 69 | 70 | $choice1 = new OptionChoice('choice1', 'test'); 71 | $choice2 = new OptionChoice('choice2', 'test2'); 72 | $choice3 = new OptionChoice('choice3', 'test3'); 73 | 74 | $option->choice($choice1); 75 | $option->choice($choice2); 76 | $option->choice($choice3); 77 | 78 | $this->assertEquals([ 79 | 'type' => CommandOption::TYPE_STRING, 80 | 'name' => $name, 81 | 'description' => $description, 82 | 'choices' => [ 83 | [ 84 | 'name' => 'choice1', 85 | 'value' => 'test', 86 | ], 87 | [ 88 | 'name' => 'choice2', 89 | 'value' => 'test2', 90 | ], 91 | [ 92 | 'name' => 'choice3', 93 | 'value' => 'test3', 94 | ], 95 | ], 96 | ], $option->toArray()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/SubCommandGroupOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_SUB_COMMAND_GROUP; 20 | $this->option = new SubCommandGroupOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new SubCommandGroupOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_SUB_COMMAND_GROUP, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new SubCommandGroupOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_SUB_COMMAND_GROUP, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/SubCommandOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_SUB_COMMAND; 20 | $this->option = new SubCommandOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new SubCommandOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_SUB_COMMAND, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new SubCommandOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_SUB_COMMAND, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/Options/UserOptionTest.php: -------------------------------------------------------------------------------- 1 | expectedType = CommandOption::TYPE_USER; 20 | $this->option = new UserOption('option', 'desc'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $description = 'test-desc'; 27 | 28 | $option = new UserOption($name, $description); 29 | 30 | $this->assertEquals([ 31 | 'type' => CommandOption::TYPE_USER, 32 | 'name' => $name, 33 | 'description' => $description, 34 | ], $option->toArray()); 35 | } 36 | 37 | public function testToArrayWithOptions() 38 | { 39 | $name = 'test-name'; 40 | $description = 'test-desc'; 41 | 42 | $option = new UserOption($name, $description); 43 | 44 | $option->required() 45 | ->nameLocalizations(['l1']) 46 | ->descriptionLocalizations(['l2']); 47 | 48 | $this->assertEquals([ 49 | 'type' => CommandOption::TYPE_USER, 50 | 'name' => $name, 51 | 'description' => $description, 52 | 'required' => true, 53 | 'name_localizations' => ['l1'], 54 | 'description_localizations' => ['l2'], 55 | ], $option->toArray()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/SlashCommandTest.php: -------------------------------------------------------------------------------- 1 | expectedType = Command::TYPE_CHAT_INPUT; 22 | $this->command = new SlashCommand('test', 'desc'); 23 | } 24 | 25 | public function testToArray() 26 | { 27 | $name = 'test-name'; 28 | $description = 'test description'; 29 | 30 | $command = new SlashCommand($name, $description); 31 | 32 | $result = $command->toArray(); 33 | $this->assertEquals([ 34 | 'type' => Command::TYPE_CHAT_INPUT, 35 | 'name' => $name, 36 | 'description' => $description, 37 | ], $result); 38 | } 39 | 40 | public function testToArrayWithOptions() 41 | { 42 | $name = 'test-name'; 43 | $description = 'test description'; 44 | $version = 'test-version'; 45 | $defaultMemPermissions = 'test-perm'; 46 | $parentAppId = 'test-app-id'; 47 | 48 | $nameLocal = ['en-us']; 49 | $descLocal = ['es']; 50 | 51 | $command = new SlashCommand($name, $description); 52 | 53 | $command->version($version); 54 | $command->dmPermission(); 55 | $command->defaultPermission(); 56 | $command->defaultMemberPermissions($defaultMemPermissions); 57 | $command->nsfw(); 58 | $command->parentApplication($parentAppId); 59 | 60 | $command->nameLocalizations($nameLocal); 61 | $command->descriptionLocalizations($descLocal); 62 | 63 | $result = $command->toArray(); 64 | $this->assertEquals([ 65 | 'type' => Command::TYPE_CHAT_INPUT, 66 | 'name' => $name, 67 | 'description' => $description, 68 | 'application_id' => $parentAppId, 69 | 'default_member_permissions' => $defaultMemPermissions, 70 | 'dm_permission' => true, 71 | 'default_permission' => true, 72 | 'nsfw' => true, 73 | 'version' => $version, 74 | 'name_localizations' => $nameLocal, 75 | 'description_localizations' => $descLocal, 76 | ], $result); 77 | } 78 | 79 | public function testToArrayWithAdditionalOptions() 80 | { 81 | $name = 'test-name'; 82 | $description = 'test description'; 83 | 84 | $optionArray = [ 85 | 'key' => 'value', 86 | ]; 87 | 88 | $option2Array = [ 89 | 'another' => 'value', 90 | ]; 91 | 92 | $command = new SlashCommand($name, $description); 93 | 94 | $option1 = \Mockery::mock(CommandOption::class); 95 | $option1->shouldReceive('toArray') 96 | ->once() 97 | ->andReturn($optionArray); 98 | 99 | $option2 = \Mockery::mock(CommandOption::class); 100 | $option2->shouldReceive('toArray') 101 | ->once() 102 | ->andReturn($option2Array); 103 | 104 | $command->option($option1); 105 | $command->option($option2); 106 | 107 | $result = $command->toArray(); 108 | $this->assertEquals([ 109 | 'type' => Command::TYPE_CHAT_INPUT, 110 | 'name' => $name, 111 | 'description' => $description, 112 | 'options' => [$optionArray, $option2Array], 113 | ], $result); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/Unit/Support/Commands/UserCommandTest.php: -------------------------------------------------------------------------------- 1 | expectedType = Command::TYPE_USER; 20 | $this->command = new UserCommand('test'); 21 | } 22 | 23 | public function testToArray() 24 | { 25 | $name = 'test-name'; 26 | $command = new UserCommand($name); 27 | 28 | $result = $command->toArray(); 29 | $this->assertEquals([ 30 | 'type' => Command::TYPE_USER, 31 | 'name' => $name, 32 | ], $result); 33 | } 34 | 35 | public function testToArrayWithOptions() 36 | { 37 | $name = 'test-name'; 38 | $version = 'test-version'; 39 | $defaultMemPermissions = 'test-perm'; 40 | $parentAppId = 'test-app-id'; 41 | 42 | $nameLocal = ['en-us']; 43 | $descLocal = ['es']; 44 | 45 | $command = new UserCommand($name); 46 | 47 | $command->version($version); 48 | $command->dmPermission(); 49 | $command->defaultPermission(); 50 | $command->defaultMemberPermissions($defaultMemPermissions); 51 | $command->nsfw(); 52 | $command->parentApplication($parentAppId); 53 | 54 | $command->nameLocalizations($nameLocal); 55 | $command->descriptionLocalizations($descLocal); 56 | 57 | $result = $command->toArray(); 58 | $this->assertEquals([ 59 | 'type' => Command::TYPE_USER, 60 | 'name' => $name, 61 | 'application_id' => $parentAppId, 62 | 'default_member_permissions' => $defaultMemPermissions, 63 | 'dm_permission' => true, 64 | 'default_permission' => true, 65 | 'nsfw' => true, 66 | 'version' => $version, 67 | 'name_localizations' => $nameLocal, 68 | 'description_localizations' => $descLocal, 69 | ], $result); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/ActionRowTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 16 | 'type' => Component::TYPE_ACTION_ROW, 17 | ], $component->toArray()); 18 | } 19 | 20 | public function testComponentWithComponents() 21 | { 22 | $expectedComponent1Array = ['k1' => 'v1']; 23 | $expectedComponent2Array = ['k2' => 'v2']; 24 | 25 | $component1 = \Mockery::mock(Component::class); 26 | $component2 = \Mockery::mock(Component::class); 27 | 28 | $component1->shouldReceive('toArray')->andReturn($expectedComponent1Array); 29 | $component2->shouldReceive('toArray')->andReturn($expectedComponent2Array); 30 | 31 | $component = new ActionRow([$component1]); 32 | $component->addComponent($component2); 33 | 34 | $this->assertEquals([ 35 | 'type' => Component::TYPE_ACTION_ROW, 36 | 'components' => [$expectedComponent1Array, $expectedComponent2Array], 37 | ], $component->toArray()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/ButtonComponentTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 22 | 'type' => Component::TYPE_BUTTON, 23 | 'style' => GenericButtonComponent::STYLE_PRIMARY, 24 | 'label' => $label, 25 | 'custom_id' => $customId, 26 | ], $component->toArray()); 27 | } 28 | 29 | public function testComponentWithOptions() 30 | { 31 | $label = 'test label'; 32 | $customId = 'custom-id'; 33 | 34 | $expectedEmojiArray = ['key' => 'value']; 35 | 36 | $emoji = \Mockery::mock(EmojiObject::class); 37 | $emoji->shouldReceive('toArray')->andReturn($expectedEmojiArray); 38 | 39 | $component = new ButtonComponent($label, $customId); 40 | $component->withEmoji($emoji); 41 | $component->disabled(); 42 | 43 | $this->assertEquals([ 44 | 'type' => Component::TYPE_BUTTON, 45 | 'style' => GenericButtonComponent::STYLE_PRIMARY, 46 | 'label' => $label, 47 | 'custom_id' => $customId, 48 | 'disabled' => true, 49 | 'emoji' => $expectedEmojiArray, 50 | ], $component->toArray()); 51 | } 52 | 53 | /** 54 | * @dataProvider withStyleDataProvider 55 | */ 56 | public function testComponentWithStyle(int $expectedStyle) 57 | { 58 | $label = 'test label'; 59 | $customId = 'custom-id'; 60 | 61 | $component = new ButtonComponent($label, $customId); 62 | 63 | switch ($expectedStyle) { 64 | case GenericButtonComponent::STYLE_PRIMARY: 65 | $component->withPrimaryStyle(); 66 | break; 67 | case GenericButtonComponent::STYLE_SECONDARY: 68 | $component->withSecondaryStyle(); 69 | break; 70 | case GenericButtonComponent::STYLE_SUCCESS: 71 | $component->withSuccessStyle(); 72 | break; 73 | case GenericButtonComponent::STYLE_DANGER: 74 | $component->withDangerStyle(); 75 | break; 76 | } 77 | 78 | $this->assertEquals([ 79 | 'type' => Component::TYPE_BUTTON, 80 | 'style' => $expectedStyle, 81 | 'label' => $label, 82 | 'custom_id' => $customId, 83 | ], $component->toArray()); 84 | } 85 | 86 | public function withStyleDataProvider(): array 87 | { 88 | return [ 89 | [GenericButtonComponent::STYLE_PRIMARY], 90 | [GenericButtonComponent::STYLE_SECONDARY], 91 | [GenericButtonComponent::STYLE_SUCCESS], 92 | [GenericButtonComponent::STYLE_DANGER], 93 | ]; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/LinkButtonComponentTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 22 | 'type' => Component::TYPE_BUTTON, 23 | 'style' => GenericButtonComponent::STYLE_LINK, 24 | 'label' => $label, 25 | 'url' => $url, 26 | ], $component->toArray()); 27 | } 28 | 29 | public function testComponentWithOptions() 30 | { 31 | $label = 'test label'; 32 | $url = 'https://example.com'; 33 | 34 | $expectedEmojiArray = ['key' => 'value']; 35 | 36 | $emoji = \Mockery::mock(EmojiObject::class); 37 | $emoji->shouldReceive('toArray')->andReturn($expectedEmojiArray); 38 | 39 | $component = new LinkButtonComponent($label, $url); 40 | $component->withEmoji($emoji); 41 | $component->disabled(); 42 | 43 | $this->assertEquals([ 44 | 'type' => Component::TYPE_BUTTON, 45 | 'style' => GenericButtonComponent::STYLE_LINK, 46 | 'label' => $label, 47 | 'disabled' => true, 48 | 'url' => $url, 49 | 'emoji' => $expectedEmojiArray, 50 | ], $component->toArray()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/ParagraphTextInputComponentTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 21 | 'type' => Component::TYPE_TEXT_INPUT, 22 | 'style' => GenericTextInputComponent::STYLE_PARAGRAPH, 23 | 'custom_id' => $customId, 24 | 'label' => $label, 25 | ], $component->toArray()); 26 | } 27 | 28 | public function testComponentWithOptions() 29 | { 30 | $label = 'label'; 31 | $customId = 'custom-id'; 32 | 33 | $minLength = 5; 34 | $maxLength = 10; 35 | $placeholder = 'test placeholder'; 36 | $value = 'test value'; 37 | 38 | $component = new ParagraphTextInputComponent($label, $customId); 39 | 40 | $component->withPlaceholder($placeholder); 41 | $component->withMinLength($minLength); 42 | $component->withMaxLength($maxLength); 43 | $component->withValue($value); 44 | $component->required(); 45 | 46 | $this->assertEquals([ 47 | 'type' => Component::TYPE_TEXT_INPUT, 48 | 'style' => GenericTextInputComponent::STYLE_PARAGRAPH, 49 | 'custom_id' => $customId, 50 | 'label' => $label, 51 | 'min_length' => $minLength, 52 | 'max_length' => $maxLength, 53 | 'placeholder' => $placeholder, 54 | 'value' => $value, 55 | 'required' => true, 56 | ], $component->toArray()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/SelectMenuComponentTest.php: -------------------------------------------------------------------------------- 1 | 'v1']; 18 | $expectedOption2Array = ['k2' => 'v2']; 19 | 20 | $option1 = \Mockery::mock(SelectOptionObject::class); 21 | $option2 = \Mockery::mock(SelectOptionObject::class); 22 | 23 | $option1->shouldReceive('toArray')->andReturn($expectedOption1Array); 24 | $option2->shouldReceive('toArray')->andReturn($expectedOption2Array); 25 | 26 | $component = new SelectMenuComponent($customId, [$option1, $option2]); 27 | 28 | $this->assertEquals([ 29 | 'type' => Component::TYPE_SELECT_MENU, 30 | 'custom_id' => $customId, 31 | 'options' => [$expectedOption1Array, $expectedOption2Array], 32 | ], $component->toArray()); 33 | } 34 | 35 | public function testComponentWithOptions() 36 | { 37 | $customId = 'custom-id'; 38 | 39 | $expectedOption1Array = ['k1' => 'v1']; 40 | $expectedOption2Array = ['k2' => 'v2']; 41 | 42 | $option1 = \Mockery::mock(SelectOptionObject::class); 43 | $option2 = \Mockery::mock(SelectOptionObject::class); 44 | 45 | $option1->shouldReceive('toArray')->andReturn($expectedOption1Array); 46 | $option2->shouldReceive('toArray')->andReturn($expectedOption2Array); 47 | 48 | $component = new SelectMenuComponent($customId, [$option1, $option2]); 49 | $component->withPlaceholder('test placeholder'); 50 | $component->withMinValues(5); 51 | $component->withMaxValues(10); 52 | $component->disabled(); 53 | 54 | $this->assertEquals([ 55 | 'type' => Component::TYPE_SELECT_MENU, 56 | 'custom_id' => $customId, 57 | 'options' => [$expectedOption1Array, $expectedOption2Array], 58 | 'placeholder' => 'test placeholder', 59 | 'min_values' => 5, 60 | 'max_values' => 10, 61 | 'disabled' => true, 62 | ], $component->toArray()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Unit/Support/Components/ShortTextInputComponentTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 21 | 'type' => Component::TYPE_TEXT_INPUT, 22 | 'style' => GenericTextInputComponent::STYLE_SHORT, 23 | 'custom_id' => $customId, 24 | 'label' => $label, 25 | ], $component->toArray()); 26 | } 27 | 28 | public function testComponentWithOptions() 29 | { 30 | $label = 'label'; 31 | $customId = 'custom-id'; 32 | 33 | $minLength = 5; 34 | $maxLength = 10; 35 | $placeholder = 'test placeholder'; 36 | $value = 'test value'; 37 | 38 | $component = new ShortTextInputComponent($label, $customId); 39 | 40 | $component->withPlaceholder($placeholder); 41 | $component->withMinLength($minLength); 42 | $component->withMaxLength($maxLength); 43 | $component->withValue($value); 44 | $component->required(); 45 | 46 | $this->assertEquals([ 47 | 'type' => Component::TYPE_TEXT_INPUT, 48 | 'style' => GenericTextInputComponent::STYLE_SHORT, 49 | 'custom_id' => $customId, 50 | 'label' => $label, 51 | 'min_length' => $minLength, 52 | 'max_length' => $maxLength, 53 | 'placeholder' => $placeholder, 54 | 'value' => $value, 55 | 'required' => true, 56 | ], $component->toArray()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/AuthorEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 18 | 'type' => Embed::TYPE_AUTHOR, 19 | 'author' => [ 20 | 'name' => $name, 21 | ], 22 | ], $embed->toArray()); 23 | } 24 | 25 | public function testEmbedWithOptions() 26 | { 27 | $name = 'test name'; 28 | $title = 'test title'; 29 | $description = 'test description'; 30 | $timestamp = '12345'; 31 | 32 | $url = 'https://example.com'; 33 | $iconUrl = 'https://example.com/icon'; 34 | $proxyIconUrl = 'https://example.com/proxy'; 35 | 36 | $embed = new AuthorEmbed($name, $title, $description, $timestamp); 37 | 38 | $embed->withUrl($url); 39 | $embed->withIconUrl($iconUrl); 40 | $embed->withProxyIconUrl($proxyIconUrl); 41 | 42 | $this->assertEquals([ 43 | 'type' => Embed::TYPE_AUTHOR, 44 | 'author' => [ 45 | 'name' => $name, 46 | 'url' => $url, 47 | 'icon_url' => $iconUrl, 48 | 'proxy_icon_url' => $proxyIconUrl, 49 | ], 50 | 'title' => $title, 51 | 'description' => $description, 52 | 'timestamp' => $timestamp, 53 | ], $embed->toArray()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/FieldEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 19 | 'type' => Embed::TYPE_FIELD, 20 | 'field' => [ 21 | 'name' => $name, 22 | 'value' => $value, 23 | ], 24 | ], $embed->toArray()); 25 | } 26 | 27 | public function testEmbedWithOptions() 28 | { 29 | $name = 'test name'; 30 | $value = 'test value'; 31 | $color = 12345; 32 | $title = 'test title'; 33 | $description = 'test description'; 34 | $timestamp = '12345'; 35 | 36 | $embed = new FieldEmbed($name, $value, $title, $description, $timestamp); 37 | $embed->inline(); 38 | $embed->withColor($color); 39 | 40 | $this->assertEquals([ 41 | 'type' => Embed::TYPE_FIELD, 42 | 'field' => [ 43 | 'name' => $name, 44 | 'value' => $value, 45 | 'inline' => true, 46 | ], 47 | 'title' => $title, 48 | 'description' => $description, 49 | 'timestamp' => $timestamp, 50 | 'color' => $color, 51 | ], $embed->toArray()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/FooterEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 18 | 'type' => Embed::TYPE_FOOTER, 19 | 'footer' => [ 20 | 'text' => $text, 21 | ], 22 | ], $embed->toArray()); 23 | } 24 | 25 | public function testEmbedWithOptions() 26 | { 27 | $text = 'test text'; 28 | $title = 'test title'; 29 | $description = 'test description'; 30 | $timestamp = '12345'; 31 | 32 | $iconUrl = 'https://example.com/proxy'; 33 | $proxyIconUrl = 'https://example.com/proxy'; 34 | 35 | $embed = new FooterEmbed($text, $title, $description, $timestamp); 36 | 37 | $embed->withIconUrl($iconUrl); 38 | $embed->withProxyIconUrl($proxyIconUrl); 39 | $embed->withColor(12345); 40 | 41 | $this->assertEquals([ 42 | 'type' => Embed::TYPE_FOOTER, 43 | 'footer' => [ 44 | 'text' => $text, 45 | 'icon_url' => $iconUrl, 46 | 'proxy_icon_url' => $proxyIconUrl, 47 | ], 48 | 'title' => $title, 49 | 'description' => $description, 50 | 'timestamp' => $timestamp, 51 | 'color' => 12345, 52 | ], $embed->toArray()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/GenericEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 23 | 'type' => Embed::TYPE_RICH, 24 | 'title' => 'test title', 25 | 'description' => 'test description', 26 | ], $embed->toArray()); 27 | } 28 | 29 | public function testEmbedWithOptions() 30 | { 31 | $title = 'test title'; 32 | $description = 'test description'; 33 | 34 | $embed = new GenericEmbed($title, $description); 35 | 36 | $embed->withColor(42); 37 | 38 | $this->assertEquals([ 39 | 'type' => Embed::TYPE_RICH, 40 | 'title' => $title, 41 | 'description' => $description, 42 | 'color' => 42, 43 | ], $embed->toArray()); 44 | } 45 | 46 | public function testEmbedWithAllOptions() 47 | { 48 | $title = 'test title'; 49 | $description = 'test description'; 50 | 51 | $footer = new FooterEmbed('test footer'); 52 | $author = new AuthorEmbed('test author'); 53 | $provider = new ProviderEmbed('test provider'); 54 | $image = new ImageEmbed('test image'); 55 | $thumbnail = new ThumbnailEmbed('test thumbnail'); 56 | $video = new VideoEmbed('test video'); 57 | 58 | $embed = new GenericEmbed($title, $description); 59 | 60 | $embed->withColor(42); 61 | 62 | $embed->withFooter($footer); 63 | $embed->withAuthor($author); 64 | $embed->withProvider($provider); 65 | $embed->withImage($image); 66 | $embed->withThumbnail($thumbnail); 67 | $embed->withVideo($video); 68 | 69 | $field1 = new FieldEmbed('test field 1', 'test field 1 value'); 70 | $field2 = new FieldEmbed('test field 2', 'test field 2 value'); 71 | 72 | $embed->addField($field1) 73 | ->addField($field2); 74 | 75 | $this->assertEquals([ 76 | 'type' => Embed::TYPE_RICH, 77 | 'title' => $title, 78 | 'description' => $description, 79 | 'color' => 42, 80 | 'footer' => $footer->toArray(), 81 | 'author' => $author->toArray(), 82 | 'provider' => $provider->toArray(), 83 | 'image' => $image->toArray(), 84 | 'thumbnail' => $thumbnail->toArray(), 85 | 'video' => $video->toArray(), 86 | 'fields' => [ 87 | $field1->toArray()['field'], 88 | $field2->toArray()['field'], 89 | ], 90 | ], $embed->toArray()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/ImageEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 18 | 'type' => Embed::TYPE_IMAGE, 19 | 'image' => [ 20 | 'url' => $url, 21 | ], 22 | ], $embed->toArray()); 23 | } 24 | 25 | public function testEmbedWithOptions() 26 | { 27 | $url = 'https://example.com'; 28 | $title = 'test title'; 29 | $description = 'test description'; 30 | $timestamp = '12345'; 31 | 32 | $proxyUrl = 'https://example.com/proxy'; 33 | $height = 256; 34 | $width = 512; 35 | 36 | $embed = new ImageEmbed($url, $title, $description, $timestamp); 37 | 38 | $embed->withProxyUrl($proxyUrl); 39 | $embed->withWidth($width); 40 | $embed->withHeight($height); 41 | 42 | $this->assertEquals([ 43 | 'type' => Embed::TYPE_IMAGE, 44 | 'image' => [ 45 | 'url' => $url, 46 | 'proxy_url' => $proxyUrl, 47 | 'height' => $height, 48 | 'width' => $width, 49 | ], 50 | 'title' => $title, 51 | 'description' => $description, 52 | 'timestamp' => $timestamp, 53 | ], $embed->toArray()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/ProviderEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 16 | 'type' => Embed::TYPE_PROVIDER, 17 | ], $embed->toArray()); 18 | } 19 | 20 | public function testEmbedWithOptions() 21 | { 22 | $title = 'test title'; 23 | $description = 'test description'; 24 | $timestamp = '12345'; 25 | $url = 'https://example.com'; 26 | $name = 'test name'; 27 | 28 | $embed = new ProviderEmbed($title, $description, $timestamp); 29 | 30 | $embed->withName($name); 31 | $embed->withUrl($url); 32 | 33 | $this->assertEquals([ 34 | 'type' => Embed::TYPE_PROVIDER, 35 | 'provider' => [ 36 | 'url' => $url, 37 | 'name' => $name, 38 | ], 39 | 'title' => $title, 40 | 'description' => $description, 41 | 'timestamp' => $timestamp, 42 | ], $embed->toArray()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/ThumbnailEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 18 | 'type' => Embed::TYPE_THUMBNAIL, 19 | 'image' => [ 20 | 'url' => $url, 21 | ], 22 | ], $embed->toArray()); 23 | } 24 | 25 | public function testEmbedWithOptions() 26 | { 27 | $url = 'https://example.com'; 28 | $title = 'test title'; 29 | $description = 'test description'; 30 | $timestamp = '12345'; 31 | 32 | $proxyUrl = 'https://example.com/proxy'; 33 | $height = 256; 34 | $width = 512; 35 | 36 | $embed = new ThumbnailEmbed($url, $title, $description, $timestamp); 37 | 38 | $embed->withProxyUrl($proxyUrl); 39 | $embed->withWidth($width); 40 | $embed->withHeight($height); 41 | 42 | $this->assertEquals([ 43 | 'type' => Embed::TYPE_THUMBNAIL, 44 | 'image' => [ 45 | 'url' => $url, 46 | 'proxy_url' => $proxyUrl, 47 | 'height' => $height, 48 | 'width' => $width, 49 | ], 50 | 'title' => $title, 51 | 'description' => $description, 52 | 'timestamp' => $timestamp, 53 | ], $embed->toArray()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Support/Embeds/VideoEmbedTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 18 | 'type' => Embed::TYPE_VIDEO, 19 | 'video' => [ 20 | 'url' => $url, 21 | ], 22 | ], $embed->toArray()); 23 | } 24 | 25 | public function testEmbedWithOptions() 26 | { 27 | $url = 'https://example.com'; 28 | $title = 'test title'; 29 | $description = 'test description'; 30 | $timestamp = '12345'; 31 | 32 | $proxyUrl = 'https://example.com/proxy'; 33 | $height = 256; 34 | $width = 512; 35 | 36 | $embed = new VideoEmbed($url, $title, $description, $timestamp); 37 | 38 | $embed->withProxyUrl($proxyUrl); 39 | $embed->withWidth($width); 40 | $embed->withHeight($height); 41 | 42 | $this->assertEquals([ 43 | 'type' => Embed::TYPE_VIDEO, 44 | 'video' => [ 45 | 'url' => $url, 46 | 'proxy_url' => $proxyUrl, 47 | 'height' => $height, 48 | 'width' => $width, 49 | ], 50 | 'title' => $title, 51 | 'description' => $description, 52 | 'timestamp' => $timestamp, 53 | ], $embed->toArray()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/Support/Interactions/DiscordInteractionResponseTest.php: -------------------------------------------------------------------------------- 1 | 'value']; 15 | $type = 12; 16 | 17 | $response = new DiscordInteractionResponse($type, $data, $code); 18 | 19 | $this->assertSame($code, $response->getStatus()); 20 | $this->assertEquals([ 21 | 'type' => $type, 22 | 'data' => $data, 23 | ], $response->toArray()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Unit/Support/Interactions/Handlers/PingHandlerTest.php: -------------------------------------------------------------------------------- 1 | handle(\Mockery::mock(Request::class)); 17 | 18 | $this->assertSame(200, $result->getStatus()); 19 | $this->assertEquals([ 20 | 'type' => InteractionHandler::RESPONSE_TYPE_PONG, 21 | ], $result->toArray()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Unit/Support/Objects/AllowedMentionObjectTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([], $object->toArray()); 15 | } 16 | 17 | public function testObjectWithParseOption() 18 | { 19 | $object = new AllowedMentionObject(); 20 | 21 | $object->allowRolesMention(); 22 | $this->assertEquals([ 23 | 'parse' => [AllowedMentionObject::MENTIONS_ROLES], 24 | ], $object->toArray()); 25 | 26 | $object->allowUsersMention(); 27 | $this->assertEquals([ 28 | 'parse' => [AllowedMentionObject::MENTIONS_ROLES, AllowedMentionObject::MENTIONS_USERS], 29 | ], $object->toArray()); 30 | 31 | $object->allowEveryoneMention(); 32 | $this->assertEquals([ 33 | 'parse' => [AllowedMentionObject::MENTIONS_ROLES, AllowedMentionObject::MENTIONS_USERS, AllowedMentionObject::MENTIONS_EVERYONE], 34 | ], $object->toArray()); 35 | } 36 | 37 | public function testObjectWithAdditionalOptions() 38 | { 39 | $object = new AllowedMentionObject(); 40 | 41 | $object->allowMentionsForRoles(['role-1', 'role-2']); 42 | $object->allowMentionsForUsers(['user-1', 'user-2']); 43 | $object->mentionReplyUser(); 44 | 45 | $this->assertEquals([ 46 | 'replied_user' => true, 47 | 'roles' => ['role-1', 'role-2'], 48 | 'users' => ['user-1', 'user-2'], 49 | ], $object->toArray()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Unit/Support/Objects/EmojiObjectTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 17 | 'name' => $name, 18 | ], $object->toArray()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Support/Objects/SelectOptionObjectTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([ 20 | 'label' => $label, 21 | 'value' => $value, 22 | ], $object->toArray()); 23 | } 24 | 25 | public function testObjectWithOptions() 26 | { 27 | $label = 'test label'; 28 | $value = 'test value'; 29 | 30 | $expectedEmojiArray = ['key' => 'value']; 31 | 32 | $emoji = \Mockery::mock(EmojiObject::class); 33 | $emoji->shouldReceive('toArray')->andReturn($expectedEmojiArray); 34 | 35 | $object = new SelectOptionObject($label, $value); 36 | 37 | $object->withEmoji($emoji); 38 | $object->withDescription('test description'); 39 | $object->default(); 40 | 41 | $this->assertEquals([ 42 | 'label' => $label, 43 | 'value' => $value, 44 | 'emoji' => $expectedEmojiArray, 45 | 'default' => true, 46 | 'description' => 'test description', 47 | ], $object->toArray()); 48 | } 49 | } 50 | --------------------------------------------------------------------------------