Primer commit del sistema separado falta mejorar mucho

This commit is contained in:
nickpons666
2025-12-30 01:18:46 -06:00
commit 1679c73e52
2384 changed files with 472342 additions and 0 deletions

9
vendor/discord-php/http/.gitignore vendored Executable file
View File

@@ -0,0 +1,9 @@
/vendor/
composer.lock
test.php
.php_cs.cache
.php_cs
.php-cs-fixer.php
.php-cs-fixer.cache
.vscode
.phpunit.cache

102
vendor/discord-php/http/.php-cs-fixer.dist.php vendored Executable file
View File

@@ -0,0 +1,102 @@
<?php
$header = <<<'EOF'
This file is a part of the DiscordPHP-Http project.
Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
This file is subject to the MIT license that is bundled
with this source code in the LICENSE file.
EOF;
$fixers = [
'blank_line_after_namespace',
'braces',
'class_definition',
'elseif',
'encoding',
'full_opening_tag',
'function_declaration',
'lowercase_keywords',
'method_argument_space',
'no_closing_tag',
'no_spaces_after_function_name',
'no_spaces_inside_parenthesis',
'no_trailing_whitespace',
'no_trailing_whitespace_in_comment',
'single_blank_line_at_eof',
'single_class_element_per_statement',
'single_import_per_statement',
'single_line_after_imports',
'switch_case_semicolon_to_colon',
'switch_case_space',
'visibility_required',
'blank_line_after_opening_tag',
'no_multiline_whitespace_around_double_arrow',
'no_empty_statement',
'include',
'no_trailing_comma_in_list_call',
'not_operator_with_successor_space',
'no_leading_namespace_whitespace',
'no_blank_lines_after_class_opening',
'no_blank_lines_after_phpdoc',
'object_operator_without_whitespace',
'binary_operator_spaces',
'phpdoc_indent',
'general_phpdoc_tag_rename',
'phpdoc_inline_tag_normalizer',
'phpdoc_tag_type',
'phpdoc_no_access',
'phpdoc_no_package',
'phpdoc_scalar',
'phpdoc_summary',
'phpdoc_to_comment',
'phpdoc_trim',
'phpdoc_var_without_name',
'no_leading_import_slash',
'no_trailing_comma_in_singleline_array',
'single_blank_line_before_namespace',
'single_quote',
'no_singleline_whitespace_before_semicolons',
'cast_spaces',
'standardize_not_equals',
'ternary_operator_spaces',
'trim_array_spaces',
'unary_operator_spaces',
'no_unused_imports',
'no_useless_else',
'no_useless_return',
'phpdoc_no_empty_return',
'no_extra_blank_lines',
'multiline_whitespace_before_semicolons',
];
$rules = [
'concat_space' => ['spacing' => 'none'],
'phpdoc_no_alias_tag' => ['replacements' => ['type' => 'var']],
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => ['align_double_arrow' => true, 'align_equals' => true],
'header_comment' => ['header' => $header],
'indentation_type' => true,
'phpdoc_align' => [
'align' => 'vertical',
'tags' => ['param', 'property', 'property-read', 'property-write', 'return', 'throws', 'type', 'var', 'method'],
],
'blank_line_before_statement' => ['statements' => ['return']],
'constant_case' => ['case' => 'lower'],
'echo_tag_syntax' => ['format' => 'long'],
'trailing_comma_in_multiline' => ['elements' => ['arrays']],
];
foreach ($fixers as $fix) {
$rules[$fix] = true;
}
$config = new PhpCsFixer\Config();
return $config
->setRules($rules)
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__)
);

22
vendor/discord-php/http/LICENSE vendored Executable file
View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2021-present David Cole <david.cole1340@gmail.com> and all
contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

92
vendor/discord-php/http/README.md vendored Executable file
View File

@@ -0,0 +1,92 @@
# DiscordPHP-Http
Asynchronous HTTP client used for communication with the Discord REST API.
## Requirements
- PHP >=7.4
## Installation
```sh
$ composer require discord-php/http
```
A [psr/log](https://packagist.org/packages/psr/log)-compliant logging library is also required. We recommend [monolog](https://github.com/Seldaek/monolog) which will be used in examples.
## Usage
```php
<?php
include 'vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Discord\Http\Http;
use Discord\Http\Drivers\React;
$loop = \React\EventLoop\Factory::create();
$logger = (new Logger('logger-name'))->pushHandler(new StreamHandler('php://output'));
$http = new Http(
'Bot xxxx.yyyy.zzzz',
$loop,
$logger
);
// set up a driver - this example uses the React driver
$driver = new React($loop);
$http->setDriver($driver);
// must be the last line
$loop->run();
```
All request methods have the same footprint:
```php
$http->get(string $url, $content = null, array $headers = []);
$http->post(string $url, $content = null, array $headers = []);
$http->put(string $url, $content = null, array $headers = []);
$http->patch(string $url, $content = null, array $headers = []);
$http->delete(string $url, $content = null, array $headers = []);
```
For other methods:
```php
$http->queueRequest(string $method, string $url, $content, array $headers = []);
```
All methods return the decoded JSON response in an object:
```php
// https://discord.com/api/v8/oauth2/applications/@me
$http->get('oauth2/applications/@me')->done(function ($response) {
var_dump($response);
}, function ($e) {
echo "Error: ".$e->getMessage().PHP_EOL;
});
```
Most Discord endpoints are provided in the [Endpoint.php](src/Discord/Endpoint.php) class as constants. Parameters start with a colon,
e.g. `channels/:channel_id/messages/:message_id`. You can bind parameters to then with the same class:
```php
// channels/channel_id_here/messages/message_id_here
$endpoint = Endpoint::bind(Endpoint::CHANNEL_MESSAGE, 'channel_id_here', 'message_id_here');
$http->get($endpoint)->done(...);
```
It is recommended that if the endpoint contains parameters you use the `Endpoint::bind()` function to sort requests into their correct rate limit buckets.
For an example, see [DiscordPHP](https://github.com/discord-php/DiscordPHP).
## License
This software is licensed under the MIT license which can be viewed in the [LICENSE](LICENSE) file.
## Credits
- [David Cole](mailto:david.cole1340@gmail.com)
- All contributors

43
vendor/discord-php/http/composer.json vendored Executable file
View File

@@ -0,0 +1,43 @@
{
"name": "discord-php/http",
"description": "Handles HTTP requests to Discord servers",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "David Cole",
"email": "david.cole1340@gmail.com"
}
],
"autoload": {
"psr-4": {
"Discord\\Http\\": "src/Discord",
"Tests\\Discord\\Http\\": "tests/Discord"
}
},
"require": {
"php": "^7.4|^8.0",
"react/http": "^1.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"react/promise": "^2.2 || ^3.0.0"
},
"suggest": {
"guzzlehttp/guzzle": "For alternative to ReactPHP/Http Browser"
},
"require-dev": {
"monolog/monolog": "^2.2",
"friendsofphp/php-cs-fixer": "^2.17",
"psy/psysh": "^0.10.6",
"guzzlehttp/guzzle": "^6.0|^7.0",
"phpunit/phpunit": "^9.5",
"mockery/mockery": "^1.5",
"react/async": "^4 || ^3"
},
"scripts": {
"test": "php tests/Drivers/_server.php& HTTP_SERVER_PID=$!; ./vendor/bin/phpunit; kill $HTTP_SERVER_PID;",
"test-discord": "./vendor/bin/phpunit --testsuite Discord",
"test-drivers": "php tests/Drivers/_server.php& HTTP_SERVER_PID=$!; ./vendor/bin/phpunit --testsuite Drivers; kill $HTTP_SERVER_PID;",
"test-coverage": "php tests/Drivers/_server.php& HTTP_SERVER_PID=$!; php -d xdebug.mode=coverage ./vendor/bin/phpunit --coverage-text; kill $HTTP_SERVER_PID;",
"test-coverage-html": "php tests/Drivers/_server.php& HTTP_SERVER_PID=$!; php -d xdebug.mode=coverage ./vendor/bin/phpunit --coverage-html .phpunit.cache/cov-html; kill $HTTP_SERVER_PID;"
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
use Discord\Http\Drivers\Guzzle;
use Discord\Http\Endpoint;
use Discord\Http\Http;
use Discord\Http\Multipart\MultipartBody;
use Discord\Http\Multipart\MultipartField;
use Psr\Log\NullLogger;
use React\EventLoop\Loop;
require './vendor/autoload.php';
$http = new Http(
'Your token',
Loop::get(),
new NullLogger(),
new Guzzle(
Loop::get()
)
);
$jsonPayloadField = new MultipartField(
'json_payload',
json_encode([
'content' => 'Hello!',
]),
['Content-Type' => 'application/json']
);
$imageField = new MultipartField(
'files[0]',
file_get_contents('/path/to/image.png'),
['Content-Type' => 'image/png'],
'image.png'
);
$multipart = new MultipartBody([
$jsonPayloadField,
$imageField,
]);
$http->post(
Endpoint::bind(
Endpoint::CHANNEL_MESSAGES,
'Channel ID'
),
$multipart
)->then(
function ($response) {
// Do something with response..
},
function (Exception $e) {
echo $e->getMessage(), PHP_EOL;
}
);
Loop::run();

31
vendor/discord-php/http/phpunit.xml vendored Executable file
View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="false"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="Discord">
<directory>tests/Discord</directory>
</testsuite>
<testsuite name="Drivers">
<directory>tests/Drivers</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

226
vendor/discord-php/http/src/Discord/Bucket.php vendored Executable file
View File

@@ -0,0 +1,226 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use Composer\InstalledVersions;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use SplQueue;
/**
* Represents a rate-limit bucket.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class Bucket
{
/**
* Request queue.
*
* @var SplQueue
*/
protected $queue;
/**
* Bucket name.
*
* @var string
*/
protected $name;
/**
* ReactPHP event loop.
*
* @var LoopInterface
*/
protected $loop;
/**
* HTTP logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* Callback for when a request is ready.
*
* @var callable
*/
protected $runRequest;
/**
* Whether we are checking the queue.
*
* @var bool
*/
protected $checkerRunning = false;
/**
* Number of requests allowed before reset.
*
* @var int
*/
protected $requestLimit;
/**
* Number of remaining requests before reset.
*
* @var int
*/
protected $requestRemaining;
/**
* Timer to reset the bucket.
*
* @var TimerInterface
*/
protected $resetTimer;
/**
* Whether react/promise v3 is used, if false, using v2.
*/
protected $promiseV3 = true;
/**
* Bucket constructor.
*
* @param string $name
* @param callable $runRequest
*/
public function __construct(string $name, LoopInterface $loop, LoggerInterface $logger, callable $runRequest)
{
$this->queue = new SplQueue;
$this->name = $name;
$this->loop = $loop;
$this->logger = $logger;
$this->runRequest = $runRequest;
$this->promiseV3 = str_starts_with(InstalledVersions::getVersion('react/promise'), '3.');
}
/**
* Enqueue a request.
*
* @param Request $request
*/
public function enqueue(Request $request)
{
$this->queue->enqueue($request);
$this->logger->debug($this.' queued '.$request);
$this->checkQueue();
}
/**
* Checks for requests in the bucket.
*/
public function checkQueue()
{
// We are already checking the queue.
if ($this->checkerRunning) {
return;
}
$this->checkerRunning = true;
$this->__checkQueue();
}
protected function __checkQueue()
{
// Check for rate-limits
if ($this->requestRemaining < 1 && ! is_null($this->requestRemaining)) {
$interval = 0;
if ($this->resetTimer) {
$interval = $this->resetTimer->getInterval() ?? 0;
}
$this->logger->info($this.' expecting rate limit, timer interval '.($interval * 1000).' ms');
$this->checkerRunning = false;
return;
}
// Queue is empty, job done.
if ($this->queue->isEmpty()) {
$this->checkerRunning = false;
return;
}
/** @var Request */
$request = $this->queue->dequeue();
// Promises v3 changed `->then` to behave as `->done` and removed `->then`. We still need the behaviour of `->done` in projects using v2
($this->runRequest)($request)->{$this->promiseV3 ? 'then' : 'done'}(function (ResponseInterface $response) {
$resetAfter = (float) $response->getHeaderLine('X-Ratelimit-Reset-After');
$limit = $response->getHeaderLine('X-Ratelimit-Limit');
$remaining = $response->getHeaderLine('X-Ratelimit-Remaining');
if ($resetAfter) {
$resetAfter = (float) $resetAfter;
if ($this->resetTimer) {
$this->loop->cancelTimer($this->resetTimer);
}
$this->resetTimer = $this->loop->addTimer($resetAfter, function () {
// Reset requests remaining and check queue
$this->requestRemaining = $this->requestLimit;
$this->resetTimer = null;
$this->checkQueue();
});
}
// Check if rate-limit headers are present and store
if (is_numeric($limit)) {
$this->requestLimit = (int) $limit;
}
if (is_numeric($remaining)) {
$this->requestRemaining = (int) $remaining;
}
// Check for more requests
$this->__checkQueue();
}, function ($rateLimit) use ($request) {
if ($rateLimit instanceof RateLimit) {
$this->queue->enqueue($request);
// Bucket-specific rate-limit
// Re-queue the request and wait the retry after time
if (! $rateLimit->isGlobal()) {
$this->loop->addTimer($rateLimit->getRetryAfter(), fn () => $this->__checkQueue());
}
// Stop the queue checker for a global rate-limit.
// Will be restarted when global rate-limit finished.
else {
$this->checkerRunning = false;
$this->logger->debug($this.' stopping queue checker');
}
} else {
$this->__checkQueue();
}
});
}
/**
* Converts a bucket to a user-readable string.
*
* @return string
*/
public function __toString()
{
return 'BUCKET '.$this->name;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use Psr\Http\Message\ResponseInterface;
use React\Promise\PromiseInterface;
/**
* Interface for an HTTP driver.
*
* @author David Cole <david.cole1340@gmail.com>
*/
interface DriverInterface
{
/**
* Runs a request.
*
* Returns a promise resolved with a PSR response interface.
*
* @param Request $request
*
* @return PromiseInterface<ResponseInterface>
*/
public function runRequest(Request $request): PromiseInterface;
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Drivers;
use Discord\Http\DriverInterface;
use Discord\Http\Request;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* guzzlehttp/guzzle driver for Discord HTTP client. (still with React Promise).
*
* @author SQKo
*/
class Guzzle implements DriverInterface
{
/**
* ReactPHP event loop.
*
* @var LoopInterface|null
*/
protected $loop;
/**
* GuzzleHTTP/Guzzle client.
*
* @var Client
*/
protected $client;
/**
* Constructs the Guzzle driver.
*
* @param LoopInterface|null $loop
* @param array $options
*/
public function __construct(?LoopInterface $loop = null, array $options = [])
{
$this->loop = $loop;
// Allow 400 and 500 HTTP requests to be resolved rather than rejected.
$options['http_errors'] = false;
$this->client = new Client($options);
}
public function runRequest(Request $request): PromiseInterface
{
// Create a React promise
$deferred = new Deferred();
$reactPromise = $deferred->promise();
$promise = $this->client->requestAsync($request->getMethod(), $request->getUrl(), [
RequestOptions::HEADERS => $request->getHeaders(),
RequestOptions::BODY => $request->getContent(),
])->then([$deferred, 'resolve'], [$deferred, 'reject']);
if ($this->loop) {
$this->loop->futureTick([$promise, 'wait']);
} else {
$promise->wait();
}
return $reactPromise;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Drivers;
use Discord\Http\DriverInterface;
use Discord\Http\Request;
use React\EventLoop\LoopInterface;
use React\Http\Browser;
use React\Promise\PromiseInterface;
use React\Socket\Connector;
/**
* react/http driver for Discord HTTP client.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class React implements DriverInterface
{
/**
* ReactPHP event loop.
*
* @var LoopInterface
*/
protected $loop;
/**
* ReactPHP/HTTP browser.
*
* @var Browser
*/
protected $browser;
/**
* Constructs the React driver.
*
* @param LoopInterface $loop
* @param array $options
*/
public function __construct(LoopInterface $loop, array $options = [])
{
$this->loop = $loop;
// Allow 400 and 500 HTTP requests to be resolved rather than rejected.
$browser = new Browser($loop, new Connector($loop, $options));
$this->browser = $browser->withRejectErrorResponse(false);
}
/**
* Runs the request using the React HTTP client.
*
* @param Request $request The request to run.
*
* @return PromiseInterface
*/
public function runRequest($request): PromiseInterface
{
return $this->browser->{$request->getMethod()}(
$request->getUrl(),
$request->getHeaders(),
$request->getContent()
);
}
}

View File

@@ -0,0 +1,368 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
class Endpoint implements EndpointInterface
{
use EndpointTrait;
// GET
public const GATEWAY = 'gateway';
// GET
public const GATEWAY_BOT = self::GATEWAY.'/bot';
// GET
public const APPLICATION_SKUS = 'applications/:application_id/skus';
// GET, POST
public const APPLICATION_EMOJIS = 'applications/:application_id/emojis';
// GET, PATCH, DELETE
public const APPLICATION_EMOJI = 'applications/:application_id/emojis/:emoji_id';
// GET, POST
public const APPLICATION_ENTITLEMENTS = 'applications/:application_id/entitlements';
// DELETE
public const APPLICATION_ENTITLEMENT = self::APPLICATION_ENTITLEMENTS.'/:entitlement_id';
// POST
public const APPLICATION_ENTITLEMENT_CONSUME = self::APPLICATION_ENTITLEMENT.'/consume';
// GET, POST, PUT
public const GLOBAL_APPLICATION_COMMANDS = 'applications/:application_id/commands';
// GET, PATCH, DELETE
public const GLOBAL_APPLICATION_COMMAND = self::GLOBAL_APPLICATION_COMMANDS.'/:command_id';
// GET, POST, PUT
public const GUILD_APPLICATION_COMMANDS = 'applications/:application_id/guilds/:guild_id/commands';
// GET, PUT
public const GUILD_APPLICATION_COMMANDS_PERMISSIONS = self::GUILD_APPLICATION_COMMANDS.'/permissions';
// GET, PATCH, DELETE
public const GUILD_APPLICATION_COMMAND = self::GUILD_APPLICATION_COMMANDS.'/:command_id';
// GET, PUT
public const GUILD_APPLICATION_COMMAND_PERMISSIONS = self::GUILD_APPLICATION_COMMANDS.'/:command_id/permissions';
// POST
public const INTERACTION_RESPONSE = 'interactions/:interaction_id/:interaction_token/callback';
// POST
public const CREATE_INTERACTION_FOLLOW_UP = 'webhooks/:application_id/:interaction_token';
// PATCH, DELETE
public const ORIGINAL_INTERACTION_RESPONSE = self::CREATE_INTERACTION_FOLLOW_UP.'/messages/@original';
// PATCH, DELETE
public const INTERACTION_FOLLOW_UP = self::CREATE_INTERACTION_FOLLOW_UP.'/messages/:message_id';
// GET
public const SKU_SUBSCRIPTIONS = '/skus/:sku_id/subscriptions';
// GET
public const SKU_SUBSCRIPTION = self::SKU_SUBSCRIPTIONS.'/:subscription_id';
// GET
public const AUDIT_LOG = 'guilds/:guild_id/audit-logs';
// GET, PATCH, DELETE
public const CHANNEL = 'channels/:channel_id';
// GET, POST
public const CHANNEL_MESSAGES = self::CHANNEL.'/messages';
// GET, PATCH, DELETE
public const CHANNEL_MESSAGE = self::CHANNEL.'/messages/:message_id';
// POST
public const CHANNEL_CROSSPOST_MESSAGE = self::CHANNEL.'/messages/:message_id/crosspost';
// POST
public const CHANNEL_MESSAGES_BULK_DELETE = self::CHANNEL.'/messages/bulk-delete';
// PUT, DELETE
public const CHANNEL_PERMISSIONS = self::CHANNEL.'/permissions/:overwrite_id';
// GET, POST
public const CHANNEL_INVITES = self::CHANNEL.'/invites';
// POST
public const CHANNEL_FOLLOW = self::CHANNEL.'/followers';
// POST
public const CHANNEL_TYPING = self::CHANNEL.'/typing';
// GET
/** @deprecated Use `CHANNEL_MESSAGES_PINS` */
public const CHANNEL_PINS = self::CHANNEL.'/pins';
// PUT, DELETE
/** @deprecated Use `CHANNEL_MESSAGES_PINS` */
public const CHANNEL_PIN = self::CHANNEL.'/pins/:message_id';
// GET
public const CHANNEL_MESSAGES_PINS = self::CHANNEL.'/messages/pins';
// PUT, DELETE
public const CHANNEL_MESSAGES_PIN = self::CHANNEL.'/messages/pins/:message_id';
// POST
public const CHANNEL_THREADS = self::CHANNEL.'/threads';
// POST
public const CHANNEL_MESSAGE_THREADS = self::CHANNEL_MESSAGE.'/threads';
// GET
public const CHANNEL_THREADS_ARCHIVED_PUBLIC = self::CHANNEL_THREADS.'/archived/public';
// GET
public const CHANNEL_THREADS_ARCHIVED_PRIVATE = self::CHANNEL_THREADS.'/archived/private';
// GET
public const CHANNEL_THREADS_ARCHIVED_PRIVATE_ME = self::CHANNEL.'/users/@me/threads/archived/private';
// POST
public const CHANNEL_SEND_SOUNDBOARD_SOUND = self::CHANNEL.'/send-soundboard-sound';
// GET, PATCH, DELETE
public const THREAD = 'channels/:thread_id';
// GET
public const THREAD_MEMBERS = self::THREAD.'/thread-members';
// GET, PUT, DELETE
public const THREAD_MEMBER = self::THREAD_MEMBERS.'/:user_id';
// PUT, DELETE
public const THREAD_MEMBER_ME = self::THREAD_MEMBERS.'/@me';
// GET, DELETE
public const MESSAGE_REACTION_ALL = self::CHANNEL.'/messages/:message_id/reactions';
// GET, DELETE
public const MESSAGE_REACTION_EMOJI = self::CHANNEL.'/messages/:message_id/reactions/:emoji';
// PUT, DELETE
public const OWN_MESSAGE_REACTION = self::CHANNEL.'/messages/:message_id/reactions/:emoji/@me';
// DELETE
public const USER_MESSAGE_REACTION = self::CHANNEL.'/messages/:message_id/reactions/:emoji/:user_id';
// GET
protected const MESSAGE_POLL = self::CHANNEL.'/polls/:message_id';
// GET
public const MESSAGE_POLL_ANSWER = self::MESSAGE_POLL.'/answers/:answer_id';
// POST
public const MESSAGE_POLL_EXPIRE = self::MESSAGE_POLL.'/expire';
// GET, POST
public const CHANNEL_WEBHOOKS = self::CHANNEL.'/webhooks';
// POST
public const GUILDS = 'guilds';
// GET, PATCH, DELETE
public const GUILD = 'guilds/:guild_id';
// GET, POST, PATCH
public const GUILD_CHANNELS = self::GUILD.'/channels';
// GET
public const GUILD_THREADS_ACTIVE = self::GUILD.'/threads/active';
// GET
public const GUILD_MESSAGES_SEARCH = self::GUILD.'/messages/search';
// GET
public const GUILD_MEMBERS = self::GUILD.'/members';
// GET
public const GUILD_MEMBERS_SEARCH = self::GUILD.'/members/search';
// GET, PATCH, PUT, DELETE
public const GUILD_MEMBER = self::GUILD.'/members/:user_id';
// PATCH
public const GUILD_MEMBER_SELF = self::GUILD.'/members/@me';
/** @deprecated 9.0.9 Use `GUILD_MEMBER_SELF` */
public const GUILD_MEMBER_SELF_NICK = self::GUILD.'/members/@me/nick';
// PUT, DELETE
public const GUILD_MEMBER_ROLE = self::GUILD.'/members/:user_id/roles/:role_id';
// GET
public const GUILD_BANS = self::GUILD.'/bans';
// GET, PUT, DELETE
public const GUILD_BAN = self::GUILD.'/bans/:user_id';
// POST
public const GUILD_BAN_BULK = self::GUILD.'/bulk-ban';
// GET, PATCH
public const GUILD_ROLES = self::GUILD.'/roles';
// GET
public const GUILD_ROLES_MEMBER_COUNTS = self::GUILD.'/roles/member-counts';
// GET, POST, PATCH, DELETE
public const GUILD_ROLE = self::GUILD.'/roles/:role_id';
// POST
public const GUILD_MFA = self::GUILD.'/mfa';
// GET, POST
public const GUILD_INVITES = self::GUILD.'/invites';
// GET, POST
public const GUILD_INTEGRATIONS = self::GUILD.'/integrations';
// PATCH, DELETE
public const GUILD_INTEGRATION = self::GUILD.'/integrations/:integration_id';
// POST
public const GUILD_INTEGRATION_SYNC = self::GUILD.'/integrations/:integration_id/sync';
// GET, POST
public const GUILD_EMOJIS = self::GUILD.'/emojis';
// GET, PATCH, DELETE
public const GUILD_EMOJI = self::GUILD.'/emojis/:emoji_id';
// GET
public const GUILD_PREVIEW = self::GUILD.'/preview';
// GET, POST
public const GUILD_PRUNE = self::GUILD.'/prune';
// GET
public const GUILD_REGIONS = self::GUILD.'/regions';
// GET, PATCH
public const GUILD_WIDGET_SETTINGS = self::GUILD.'/widget';
// GET
public const GUILD_WIDGET = self::GUILD.'/widget.json';
// GET
public const GUILD_WIDGET_IMAGE = self::GUILD.'/widget.png';
// GET, PATCH
public const GUILD_WELCOME_SCREEN = self::GUILD.'/welcome-screen';
// GET
public const GUILD_ONBOARDING = self::GUILD.'/onboarding';
// GET
public const LIST_VOICE_REGIONS = 'voice/regions';
// GET, PATCH
public const GUILD_USER_CURRENT_VOICE_STATE = self::GUILD.'/voice-states/@me';
// GET, PATCH
public const GUILD_USER_VOICE_STATE = self::GUILD.'/voice-states/:user_id';
// GET
public const GUILD_VANITY_URL = self::GUILD.'/vanity-url';
// GET, PATCH
public const GUILD_MEMBERSHIP_SCREENING = self::GUILD.'/member-verification';
// GET
public const GUILD_WEBHOOKS = self::GUILD.'/webhooks';
// GET, POST
public const GUILD_STICKERS = self::GUILD.'/stickers';
// GET, PATCH, DELETE
public const GUILD_STICKER = self::GUILD.'/stickers/:sticker_id';
// GET
public const STICKER = 'stickers/:sticker_id';
// GET
public const STICKER_PACKS = 'sticker-packs';
// GET, POST
public const GUILD_SCHEDULED_EVENTS = self::GUILD.'/scheduled-events';
// GET, PATCH, DELETE
public const GUILD_SCHEDULED_EVENT = self::GUILD.'/scheduled-events/:guild_scheduled_event_id';
// GET
public const GUILD_SCHEDULED_EVENT_USERS = self::GUILD.'/scheduled-events/:guild_scheduled_event_id/users';
// GET, POST
public const GUILD_SOUNDBOARD_SOUNDS = self::GUILD.'/soundboard-sounds';
// GET, PATCH, DELETE
public const GUILD_SOUNDBOARD_SOUND = self::GUILD.'/soundboard-sounds/:sound_id';
// GET, DELETE
public const INVITE = 'invites/:code';
// POST
public const STAGE_INSTANCES = 'stage-instances';
// GET, PATCH, DELETE
public const STAGE_INSTANCE = 'stage-instances/:channel_id';
// GET, POST
public const GUILDS_TEMPLATE = self::GUILDS.'/templates/:template_code';
// GET, POST
public const GUILD_TEMPLATES = self::GUILD.'/templates';
// PUT, PATCH, DELETE
public const GUILD_TEMPLATE = self::GUILD.'/templates/:template_code';
// GET, POST
public const GUILD_AUTO_MODERATION_RULES = self::GUILD.'/auto-moderation/rules';
// GET, PATCH, DELETE
public const GUILD_AUTO_MODERATION_RULE = self::GUILD.'/auto-moderation/rules/:auto_moderation_rule_id';
// POST
public const LOBBIES = 'lobbies';
// GET, PATCH, DELETE
public const LOBBY = self::LOBBIES.'/:lobby_id';
// PUT, DELETE
public const LOBBY_MEMBER = self::LOBBY.'/members/:user_id/';
// DELETE
public const LOBBY_SELF = self::LOBBY.'/members/@me';
// PATCH
public const LOBBY_CHANNEL_LINKING = self::LOBBY.'/channel-linking';
// GET
public const SOUNDBOARD_DEFAULT_SOUNDS = 'soundboard-default-sounds';
// GET, PATCH
public const USER_CURRENT = 'users/@me';
// GET
public const USER = 'users/:user_id';
// GET
public const USER_CURRENT_GUILDS = self::USER_CURRENT.'/guilds';
// DELETE
public const USER_CURRENT_GUILD = self::USER_CURRENT.'/guilds/:guild_id';
// GET
public const USER_CURRENT_MEMBER = self::USER_CURRENT_GUILD.'/member';
// GET, POST
public const USER_CURRENT_CHANNELS = self::USER_CURRENT.'/channels';
// GET
public const USER_CURRENT_CONNECTIONS = self::USER_CURRENT.'/connections';
// GET, PUT
public const USER_CURRENT_APPLICATION_ROLE_CONNECTION = self::USER_CURRENT.'/applications/:application_id/role-connection';
// GET, PATCH
public const APPLICATION_CURRENT = 'applications/@me';
// GET
public const APPLICATION_ACTIVITY_INSTANCE = 'applications/:application_id/activity-instances/:instance_id';
// GET, PATCH, DELETE
public const WEBHOOK = 'webhooks/:webhook_id';
// GET, PATCH, DELETE
public const WEBHOOK_TOKEN = 'webhooks/:webhook_id/:webhook_token';
// POST
public const WEBHOOK_EXECUTE = self::WEBHOOK_TOKEN;
// POST
public const WEBHOOK_EXECUTE_SLACK = self::WEBHOOK_EXECUTE.'/slack';
// POST
public const WEBHOOK_EXECUTE_GITHUB = self::WEBHOOK_EXECUTE.'/github';
// PATCH, DELETE
public const WEBHOOK_MESSAGE = self::WEBHOOK_TOKEN.'/messages/:message_id';
// GET, PUT
public const APPLICATION_ROLE_CONNECTION_METADATA = 'applications/:application_id/role-connections/metadata';
/**
* Regex to identify parameters in endpoints.
*
* @var string
*/
public const REGEX = '/:([^\/]*)/';
/**
* A list of parameters considered 'major' by Discord.
*
* @see https://discord.com/developers/docs/topics/rate-limits
* @var string[]
*/
public const MAJOR_PARAMETERS = ['channel_id', 'guild_id', 'webhook_id', 'thread_id'];
/**
* The string version of the endpoint, including all parameters.
*
* @var string
*/
protected $endpoint;
/**
* Array of placeholders to be replaced in the endpoint.
*
* @var string[]
*/
protected $vars = [];
/**
* Array of arguments to substitute into the endpoint.
*
* @var string[]
*/
protected $args = [];
/**
* Array of query data to be appended
* to the end of the endpoint with `http_build_query`.
*
* @var array
*/
protected $query = [];
/**
* Creates an endpoint class.
*
* @param string $endpoint
*/
public function __construct(string $endpoint)
{
$this->endpoint = $endpoint;
if (preg_match_all(self::REGEX, $endpoint, $vars)) {
$this->vars = $vars[1] ?? [];
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
interface EndpointInterface
{
public function bindArgs(...$args): self;
public function bindAssoc(array $args): self;
public function addQuery(string $key, $value): void;
public function toAbsoluteEndpoint(bool $onlyMajorParameters = false): string;
public function __toString(): string;
public static function bind(string $endpoint, ...$args);
}

View File

@@ -0,0 +1,137 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
trait EndpointTrait
{
/**
* Binds a list of arguments to the endpoint.
*
* @param string[] ...$args
* @return this
*/
public function bindArgs(...$args): self
{
for ($i = 0; $i < count($this->vars) && $i < count($args); $i++) {
$this->args[$this->vars[$i]] = $args[$i];
}
return $this;
}
/**
* Binds an associative array to the endpoint.
*
* @param string[] $args
* @return this
*/
public function bindAssoc(array $args): self
{
$this->args = array_merge($this->args, $args);
return $this;
}
/**
* Adds a key-value query pair to the endpoint.
*
* @param string $key
* @param string|bool $value
*/
public function addQuery(string $key, $value): void
{
if (! is_bool($value)) {
$value = is_array($value)
? (implode(' ', $value))
: (string) $value;
}
$this->query[$key] = $value;
}
/**
* Converts the endpoint into the absolute endpoint with
* placeholders replaced.
*
* Passing a true boolean in will only replace the major parameters.
* Used for rate limit buckets.
*
* @param bool $onlyMajorParameters
* @return string
*/
public function toAbsoluteEndpoint(bool $onlyMajorParameters = false): string
{
$endpoint = $this->endpoint;
// Process in order of longest to shortest variable name to prevent partial replacements (see #16).
$vars = $this->vars;
usort($vars, fn ($a, $b) => strlen($b) <=> strlen($a));
foreach ($vars as $var) {
if (
! isset($this->args[$var]) ||
(
$onlyMajorParameters &&
(method_exists($this, 'isMajorParameter') ? ! $this->isMajorParameter($var) : false)
)
) {
continue;
}
$endpoint = str_replace(":{$var}", $this->args[$var], $endpoint);
}
if (! $onlyMajorParameters && count($this->query) > 0) {
$endpoint .= '?'.http_build_query($this->query);
}
return $endpoint;
}
/**
* Converts the endpoint to a string.
* Alias of ->toAbsoluteEndpoint();.
*
* @return string
*/
public function __toString(): string
{
return $this->toAbsoluteEndpoint();
}
/**
* Creates an endpoint class and binds arguments to
* the newly created instance.
*
* @param string $endpoint
* @param string[] $args
* @return Endpoint
*/
public static function bind(string $endpoint, ...$args)
{
$endpoint = new Endpoint($endpoint);
$endpoint->bindArgs(...$args);
return $endpoint;
}
/**
* Checks if a parameter is a major parameter.
*
* @param string $param
* @return bool
*/
private static function isMajorParameter(string $param): bool
{
return in_array($param, Endpoint::MAJOR_PARAMETERS);
}
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when a request to Discord's REST API returned ClientErrorResponse.
*
* @author SQKo
*/
class BadRequestException extends RequestFailedException
{
protected $code = 400;
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when the Discord servers return `content longer than 2000 characters` after
* a REST request. The user must use WebSockets to obtain this data if they need it.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class ContentTooLongException extends RequestFailedException
{
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when an invalid token is provided to a Discord endpoint.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class InvalidTokenException extends RequestFailedException
{
protected $code = 401;
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when a request to Discord's REST API method is invalid.
*
* @author SQKo
*/
class MethodNotAllowedException extends RequestFailedException
{
protected $code = 405;
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when you do not have permissions to do something.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class NoPermissionsException extends RequestFailedException
{
protected $code = 403;
}

View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when a 404 Not Found response is received.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class NotFoundException extends RequestFailedException
{
protected $code = 404;
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
/**
* Thrown when a request to Discord's REST API got rate limited and the library
* does not know how to handle.
*
* @author SQKo
*/
class RateLimitException extends RequestFailedException
{
protected $code = 429;
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Exceptions;
use RuntimeException;
/**
* Thrown when a request to Discord's REST API fails.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class RequestFailedException extends RuntimeException
{
}

152
vendor/discord-php/http/src/Discord/Http.php vendored Executable file
View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use Composer\InstalledVersions;
use Psr\Log\LoggerInterface;
use React\EventLoop\LoopInterface;
use SplQueue;
/**
* Discord HTTP client.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class Http implements HttpInterface
{
use HttpTrait;
/**
* DiscordPHP-Http version.
*
* @var string
*/
public const VERSION = 'v10.5.1';
/**
* Current Discord HTTP API version.
*
* @var string
*/
public const HTTP_API_VERSION = 10;
/**
* Discord API base URL.
*
* @var string
*/
public const BASE_URL = 'https://discord.com/api/v'.self::HTTP_API_VERSION;
/**
* The number of concurrent requests which can
* be executed.
*
* @var int
*/
public const CONCURRENT_REQUESTS = 5;
/**
* Authentication token.
*
* @var string
*/
private $token;
/**
* Logger for HTTP requests.
*
* @var LoggerInterface
*/
protected $logger;
/**
* HTTP driver.
*
* @var DriverInterface
*/
protected $driver;
/**
* ReactPHP event loop.
*
* @var LoopInterface
*/
protected $loop;
/**
* Array of request buckets.
*
* @var Bucket[]
*/
protected $buckets = [];
/**
* The current rate-limit.
*
* @var RateLimit
*/
protected $rateLimit;
/**
* Timer that resets the current global rate-limit.
*
* @var TimerInterface
*/
protected $rateLimitReset;
/**
* Request queue to prevent API
* overload.
*
* @var SplQueue
*/
protected $queue;
/**
* Request queue to prevent API
* overload.
*
* @var SplQueue
*/
protected $unboundQueue;
/**
* Number of requests that are waiting for a response.
*
* @var int
*/
protected $waiting = 0;
/**
* Whether react/promise v3 is used, if false, using v2.
*/
protected $promiseV3 = true;
/**
* Http wrapper constructor.
*
* @param string $token
* @param LoopInterface $loop
* @param DriverInterface|null $driver
*/
public function __construct(string $token, LoopInterface $loop, LoggerInterface $logger, ?DriverInterface $driver = null)
{
$this->token = $token;
$this->loop = $loop;
$this->logger = $logger;
$this->driver = $driver;
$this->queue = new SplQueue;
$this->unboundQueue = new SplQueue;
$this->promiseV3 = str_starts_with(InstalledVersions::getVersion('react/promise'), '3.');
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use Psr\Http\Message\ResponseInterface;
use React\Promise\PromiseInterface;
/**
* Discord HTTP client.
*
* @author David Cole <david.cole1340@gmail.com>
*/
interface HttpInterface
{
public function setDriver(DriverInterface $driver): void;
public function get($url, $content = null, array $headers = []): PromiseInterface;
public function post($url, $content = null, array $headers = []): PromiseInterface;
public function put($url, $content = null, array $headers = []): PromiseInterface;
public function patch($url, $content = null, array $headers = []): PromiseInterface;
public function delete($url, $content = null, array $headers = []): PromiseInterface;
public function queueRequest(string $method, Endpoint $url, $content, array $headers = []): PromiseInterface;
public static function isUnboundEndpoint(Request $request): bool;
public function handleError(ResponseInterface $response): \Throwable;
public function getUserAgent(): string;
}

View File

@@ -0,0 +1,480 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use Discord\Http\Exceptions\BadRequestException;
use Discord\Http\Exceptions\ContentTooLongException;
use Discord\Http\Exceptions\InvalidTokenException;
use Discord\Http\Exceptions\MethodNotAllowedException;
use Discord\Http\Exceptions\NoPermissionsException;
use Discord\Http\Exceptions\NotFoundException;
use Discord\Http\Exceptions\RateLimitException;
use Discord\Http\Exceptions\RequestFailedException;
use Discord\Http\Multipart\MultipartBody;
use Psr\Http\Message\ResponseInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* Discord HTTP client.
*
* @author David Cole <david.cole1340@gmail.com>
*/
trait HttpTrait
{
/**
* Sets the driver of the HTTP client.
*
* @param DriverInterface $driver
*/
public function setDriver(DriverInterface $driver): void
{
$this->driver = $driver;
}
/**
* Runs a GET request.
*
* @param string|Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function get($url, $content = null, array $headers = []): PromiseInterface
{
if (! ($url instanceof Endpoint)) {
$url = Endpoint::bind($url);
}
return $this->queueRequest('get', $url, $content, $headers);
}
/**
* Runs a POST request.
*
* @param string|Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function post($url, $content = null, array $headers = []): PromiseInterface
{
if (! ($url instanceof Endpoint)) {
$url = Endpoint::bind($url);
}
return $this->queueRequest('post', $url, $content, $headers);
}
/**
* Runs a PUT request.
*
* @param string|Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function put($url, $content = null, array $headers = []): PromiseInterface
{
if (! ($url instanceof Endpoint)) {
$url = Endpoint::bind($url);
}
return $this->queueRequest('put', $url, $content, $headers);
}
/**
* Runs a PATCH request.
*
* @param string|Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function patch($url, $content = null, array $headers = []): PromiseInterface
{
if (! ($url instanceof Endpoint)) {
$url = Endpoint::bind($url);
}
return $this->queueRequest('patch', $url, $content, $headers);
}
/**
* Runs a DELETE request.
*
* @param string|Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function delete($url, $content = null, array $headers = []): PromiseInterface
{
if (! ($url instanceof Endpoint)) {
$url = Endpoint::bind($url);
}
return $this->queueRequest('delete', $url, $content, $headers);
}
/**
* Builds and queues a request.
*
* @param string $method
* @param Endpoint $url
* @param mixed $content
* @param array $headers
*
* @return PromiseInterface
*/
public function queueRequest(string $method, Endpoint $url, $content, array $headers = []): PromiseInterface
{
$deferred = new Deferred();
if (is_null($this->driver)) {
$deferred->reject(new \Exception('HTTP driver is missing.'));
return $deferred->promise();
}
$headers = array_merge($headers, [
'User-Agent' => $this->getUserAgent(),
'Authorization' => $this->token,
'X-Ratelimit-Precision' => 'millisecond',
]);
$baseHeaders = [
'User-Agent' => $this->getUserAgent(),
'Authorization' => $this->token,
'X-Ratelimit-Precision' => 'millisecond',
];
if (! is_null($content) && ! isset($headers['Content-Type'])) {
$baseHeaders = array_merge(
$baseHeaders,
$this->guessContent($content)
);
}
$headers = array_merge($baseHeaders, $headers);
$request = new Request($deferred, $method, $url, $content ?? '', $headers);
$this->sortIntoBucket($request);
return $deferred->promise();
}
/**
* Guesses the headers and transforms the content of a request.
*
* @param mixed $content
*/
protected function guessContent(&$content)
{
if ($content instanceof MultipartBody) {
$headers = $content->getHeaders();
$content = (string) $content;
return $headers;
}
$content = json_encode($content);
return [
'Content-Type' => 'application/json',
'Content-Length' => strlen($content),
];
}
/**
* Executes a request.
*
* @param Request $request
* @param Deferred|null $deferred
*
* @return PromiseInterface
*/
protected function executeRequest(Request $request, ?Deferred $deferred = null): PromiseInterface
{
if ($deferred === null) {
$deferred = new Deferred();
}
if ($this->rateLimit) {
$deferred->reject($this->rateLimit);
return $deferred->promise();
}
// Promises v3 changed `->then` to behave as `->done` and removed `->then`. We still need the behaviour of `->done` in projects using v2
$this->driver->runRequest($request)->{$this->promiseV3 ? 'then' : 'done'}(function (ResponseInterface $response) use ($request, $deferred) {
$data = json_decode((string) $response->getBody());
$statusCode = $response->getStatusCode();
// Discord Rate-limit
if ($statusCode == 429) {
if (! isset($data->global)) {
if ($response->hasHeader('X-RateLimit-Global')) {
$data->global = $response->getHeader('X-RateLimit-Global')[0] == 'true';
} else {
// Some other 429
$this->logger->error($request.' does not contain global rate-limit value');
$rateLimitError = new RateLimitException('No rate limit global response', $statusCode);
$deferred->reject($rateLimitError);
$request->getDeferred()->reject($rateLimitError);
return;
}
}
if (! isset($data->retry_after)) {
if ($response->hasHeader('Retry-After')) {
$data->retry_after = $response->getHeader('Retry-After')[0];
} else {
// Some other 429
$this->logger->error($request.' does not contain retry after rate-limit value');
$rateLimitError = new RateLimitException('No rate limit retry after response', $statusCode);
$deferred->reject($rateLimitError);
$request->getDeferred()->reject($rateLimitError);
return;
}
}
$rateLimit = new RateLimit($data->global, $data->retry_after);
$this->logger->warning($request.' hit rate-limit: '.$rateLimit);
if ($rateLimit->isGlobal() && ! $this->rateLimit) {
$this->rateLimit = $rateLimit;
$this->rateLimitReset = $this->loop->addTimer($rateLimit->getRetryAfter(), function () {
$this->rateLimit = null;
$this->rateLimitReset = null;
$this->logger->info('global rate-limit reset');
// Loop through all buckets and check for requests
foreach ($this->buckets as $bucket) {
$bucket->checkQueue();
}
});
}
$deferred->reject($rateLimit->isGlobal() ? $this->rateLimit : $rateLimit);
}
// Bad Gateway
// Cloudflare SSL Handshake error
// Push to the back of the bucket to be retried.
elseif ($statusCode == 502 || $statusCode == 525) {
$this->logger->warning($request.' 502/525 - retrying request');
$this->executeRequest($request, $deferred);
}
// Any other unsuccessful status codes
elseif ($statusCode < 200 || $statusCode >= 300) {
$error = $this->handleError($response);
$this->logger->warning($request.' failed: '.$error);
$deferred->reject($error);
$request->getDeferred()->reject($error);
}
// All is well
else {
$this->logger->debug($request.' successful');
$deferred->resolve($response);
$request->getDeferred()->resolve($data);
}
}, function (\Exception $e) use ($request, $deferred) {
$this->logger->warning($request.' failed: '.$e->getMessage());
$deferred->reject($e);
$request->getDeferred()->reject($e);
});
return $deferred->promise();
}
/**
* Sorts a request into a bucket.
*
* @param Request $request
*/
protected function sortIntoBucket(Request $request): void
{
$bucket = $this->getBucket($request->getBucketID());
$bucket->enqueue($request);
}
/**
* Gets a bucket.
*
* @param string $key
*
* @return Bucket
*/
protected function getBucket(string $key): Bucket
{
if (! isset($this->buckets[$key])) {
$bucket = new Bucket($key, $this->loop, $this->logger, function (Request $request) {
$deferred = new Deferred();
self::isUnboundEndpoint($request)
? $this->unboundQueue->enqueue([$request, $deferred])
: $this->queue->enqueue([$request, $deferred]);
$this->checkQueue();
return $deferred->promise();
});
$this->buckets[$key] = $bucket;
}
return $this->buckets[$key];
}
/**
* Checks the request queue to see if more requests can be
* sent out.
*/
protected function checkQueue(bool $check_interactions = true): void
{
if ($check_interactions) {
$this->checkunboundQueue();
}
if ($this->waiting >= Http::CONCURRENT_REQUESTS || $this->queue->isEmpty()) {
$this->logger->debug('http not checking queue', ['waiting' => $this->waiting, 'empty' => $this->queue->isEmpty()]);
return;
}
/**
* @var Request $request
* @var Deferred $deferred
*/
[$request, $deferred] = $this->queue->dequeue();
++$this->waiting;
$this->executeRequest($request)->then(function ($result) use ($deferred) {
--$this->waiting;
$this->checkQueue(false);
$deferred->resolve($result);
}, function ($e) use ($deferred) {
--$this->waiting;
$this->checkQueue(false);
$deferred->reject($e);
});
}
/**
* Checks the interaction queue to see if more requests can be
* sent out.
*/
protected function checkunboundQueue(): void
{
if ($this->unboundQueue->isEmpty()) {
$this->logger->debug('http not checking interaction queue', ['waiting' => $this->waiting, 'empty' => $this->unboundQueue->isEmpty()]);
return;
}
/**
* @var Request $request
* @var Deferred $deferred
*/
[$request, $deferred] = $this->unboundQueue->dequeue();
$this->executeRequest($request)->then(function ($result) use ($deferred) {
$this->checkQueue();
$deferred->resolve($result);
}, function ($e) use ($deferred) {
$this->checkQueue();
$deferred->reject($e);
});
}
/**
* Checks if the request is for an endpoint not bound by the global rate limit.
*
* @link https://discord.com/developers/docs/interactions/receiving-and-responding#endpoints
*
* @param Request $request
* @return bool
*/
public static function isUnboundEndpoint(Request $request): bool
{
$url = $request->getUrl();
return
(strpos($url, '/interactions') === 0 && strpos($url, '/callback') !== false)
|| strpos($url, '/webhooks') === 0;
}
/**
* Returns an exception based on the request.
*
* @param ResponseInterface $response
*
* @return \Throwable
*/
public function handleError(ResponseInterface $response): \Throwable
{
$reason = $response->getReasonPhrase().' - ';
$errorBody = (string) $response->getBody();
$errorCode = $response->getStatusCode();
// attempt to prettyify the response content
if (($content = json_decode($errorBody)) !== null) {
if (! empty($content->code)) {
$errorCode = $content->code;
}
$reason .= json_encode($content, JSON_PRETTY_PRINT);
} else {
$reason .= $errorBody;
}
switch ($response->getStatusCode()) {
case 400:
return new BadRequestException($reason, $errorCode);
case 401:
return new InvalidTokenException($reason, $errorCode);
case 403:
return new NoPermissionsException($reason, $errorCode);
case 404:
return new NotFoundException($reason, $errorCode);
case 405:
return new MethodNotAllowedException($reason, $errorCode);
case 500:
if (strpos(strtolower($errorBody), 'longer than 2000 characters') !== false ||
strpos(strtolower($errorBody), 'string value is too long') !== false) {
// Response was longer than 2000 characters and was blocked by Discord.
return new ContentTooLongException('Response was more than 2000 characters. Use another method to get this data.', $errorCode);
}
default:
return new RequestFailedException($reason, $errorCode);
}
}
/**
* Returns the User-Agent of the HTTP client.
*
* @return string
*/
public function getUserAgent(): string
{
return 'DiscordBot (https://github.com/discord-php/DiscordPHP-HTTP, '.Http::VERSION.')';
}
}

View File

@@ -0,0 +1,58 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Multipart;
class MultipartBody
{
public const BOUNDARY = 'DISCORDPHP-HTTP-BOUNDARY';
private array $fields;
public string $boundary;
/**
* @var MultipartField[]
*/
public function __construct(array $fields, ?string $boundary = null)
{
$this->fields = $fields;
$this->boundary = $boundary ?? self::BOUNDARY;
}
public function __toString(): string
{
$prefixedBoundary = '--'.$this->boundary;
$boundaryEnd = $prefixedBoundary.'--';
$convertedFields = array_map(
function (MultipartField $field) {
return (string) $field;
},
$this->fields
);
$fieldsString = implode(PHP_EOL.$prefixedBoundary.PHP_EOL, $convertedFields);
return implode(PHP_EOL, [
$prefixedBoundary,
$fieldsString,
$boundaryEnd,
]);
}
public function getHeaders(): array
{
return [
'Content-Type' => 'multipart/form-data; boundary='.$this->boundary,
'Content-Length' => strlen((string) $this),
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http\Multipart;
class MultipartField
{
private string $name;
private string $content;
private array $headers;
private ?string $fileName;
/**
* @var String[]
*/
public function __construct(
string $name,
string $content,
array $headers = [],
?string $fileName = null
) {
$this->name = $name;
$this->content = $content;
$this->headers = $headers;
$this->fileName = $fileName;
}
public function __toString(): string
{
$out = 'Content-Disposition: form-data; name="'.$this->name.'"';
if (! is_null($this->fileName)) {
$out .= '; filename="'.urlencode($this->fileName).'"';
}
$out .= PHP_EOL;
foreach ($this->headers as $header => $value) {
$out .= $header.': '.$value.PHP_EOL;
}
$out .= PHP_EOL.$this->content.PHP_EOL;
return $out;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use RuntimeException;
/**
* Represents a rate-limit given by Discord.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class RateLimit extends RuntimeException
{
/**
* Whether the rate-limit is global.
*
* @var bool
*/
protected $global;
/**
* Time in seconds of when to retry after.
*
* @var float
*/
protected $retry_after;
/**
* Rate limit constructor.
*
* @param bool $global
* @param float $retry_after
*/
public function __construct(bool $global, float $retry_after)
{
$this->global = $global;
$this->retry_after = $retry_after;
}
/**
* Gets the global parameter.
*
* @return bool
*/
public function isGlobal(): bool
{
return $this->global;
}
/**
* Gets the retry after parameter.
*
* @return float
*/
public function getRetryAfter(): float
{
return $this->retry_after;
}
/**
* Converts a rate-limit to a user-readable string.
*
* @return string
*/
public function __toString()
{
return 'RATELIMIT '.($this->global ? 'Global' : 'Non-global').', retry after '.$this->retry_after.' s';
}
}

View File

@@ -0,0 +1,145 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Discord\Http;
use React\Promise\Deferred;
/**
* Represents an HTTP request.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class Request
{
/**
* Deferred promise.
*
* @var Deferred
*/
protected $deferred;
/**
* Request method.
*
* @var string
*/
protected $method;
/**
* Request URL.
*
* @var Endpoint
*/
protected $url;
/**
* Request content.
*
* @var string
*/
protected $content;
/**
* Request headers.
*
* @var array
*/
protected $headers;
/**
* Request constructor.
*
* @param Deferred $deferred
* @param string $method
* @param Endpoint $url
* @param string $content
* @param array $headers
*/
public function __construct(Deferred $deferred, string $method, Endpoint $url, string $content, array $headers = [])
{
$this->deferred = $deferred;
$this->method = $method;
$this->url = $url;
$this->content = $content;
$this->headers = $headers;
}
/**
* Gets the method.
*
* @return string
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Gets the url.
*
* @return string
*/
public function getUrl(): string
{
return Http::BASE_URL.'/'.$this->url;
}
/**
* Gets the content.
*
* @return string
*/
public function getContent(): string
{
return $this->content;
}
/**
* Gets the headers.
*
* @return string
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Returns the deferred promise.
*
* @return Deferred
*/
public function getDeferred(): Deferred
{
return $this->deferred;
}
/**
* Returns the bucket ID for the request.
*
* @return string
*/
public function getBucketID(): string
{
return $this->method.$this->url->toAbsoluteEndpoint(true);
}
/**
* Converts the request to a user-readable string.
*
* @return string
*/
public function __toString()
{
return 'REQ '.strtoupper($this->method).' '.$this->url;
}
}

View File

@@ -0,0 +1,144 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http;
use Discord\Http\DriverInterface;
use Discord\Http\Request;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use function React\Async\await;
abstract class DriverInterfaceTest extends TestCase
{
abstract protected function getDriver(): DriverInterface;
private function getRequest(
string $method,
string $url,
string $content = '',
array $headers = []
): Request {
$request = Mockery::mock(Request::class);
$request->shouldReceive([
'getMethod' => $method,
'getUrl' => $url,
'getContent' => $content,
'getHeaders' => $headers,
]);
return $request;
}
/**
* @dataProvider requestProvider
*/
public function testRequest(string $method, string $url, array $content = [], array $verify = [])
{
$driver = $this->getDriver();
$request = $this->getRequest(
$method,
$url,
$content === [] ? '' : json_encode($content),
empty($content) ? [] : ['Content-Type' => 'application/json']
);
/** @var ResponseInterface */
$response = await($driver->runRequest($request));
$this->assertNotEquals('', $response->getBody());
$this->assertEquals(200, $response->getStatusCode());
$jsonDecodedBody = json_decode($response->getBody(), true);
$verify['method'] = $method;
foreach ($verify as $field => $expectedValue) {
$this->assertEquals(
$expectedValue,
$jsonDecodedBody[$field]
);
}
}
public function requestProvider(): array
{
$content = ['something' => 'value'];
return [
'Plain get' => [
'method' => 'GET',
'url' => 'http://127.0.0.1:8888',
],
'Get with params' => [
'method' => 'GET',
'url' => 'http://127.0.0.1:8888?something=value',
'verify' => [
'args' => $content,
],
],
'Plain post' => [
'method' => 'POST',
'url' => 'http://127.0.0.1:8888',
],
'Post with content' => [
'method' => 'POST',
'url' => 'http://127.0.0.1:8888',
'content' => $content,
'verify' => [
'json' => $content,
],
],
'Plain put' => [
'method' => 'PUT',
'url' => 'http://127.0.0.1:8888',
],
'Put with content' => [
'method' => 'PUT',
'url' => 'http://127.0.0.1:8888',
'content' => $content,
'verify' => [
'json' => $content,
],
],
'Plain patch' => [
'method' => 'PATCH',
'url' => 'http://127.0.0.1:8888',
],
'Patch with content' => [
'method' => 'PATCH',
'url' => 'http://127.0.0.1:8888',
'content' => $content,
'verify' => [
'json' => $content,
],
],
'Plain delete' => [
'method' => 'DELETE',
'url' => 'http://127.0.0.1:8888',
],
'Delete with content' => [
'method' => 'DELETE',
'url' => 'http://127.0.0.1:8888',
'content' => $content,
'verify' => [
'json' => $content,
],
],
];
}
}

View File

@@ -0,0 +1,174 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http;
use Discord\Http\Endpoint;
use PHPUnit\Framework\TestCase;
class EndpointTest extends TestCase
{
/**
* @dataProvider majorParamProvider
*/
public function testBindMajorParams(string $uri, array $replacements, string $expected)
{
$endpoint = new Endpoint($uri);
$endpoint->bindArgs(...$replacements);
$this->assertEquals(
$endpoint->toAbsoluteEndpoint(true),
$expected
);
}
public function majorParamProvider(): array
{
return [
'Several major params' => [
'uri' => 'something/:guild_id/:channel_id/:webhook_id',
'replacements' => ['::guild id::', '::channel id::', '::webhook id::'],
'expected' => 'something/::guild id::/::channel id::/::webhook id::',
],
'Single major param' => [
'uri' => 'something/:guild_id',
'replacements' => ['::guild id::'],
'expected' => 'something/::guild id::',
],
'Single major param, some minor params' => [
'uri' => 'something/:guild_id/:some_param/:something_else',
'replacements' => ['::guild id::', '::some_param::', '::something else::'],
'expected' => 'something/::guild id::/:some_param/:something_else',
],
'Only minor params' => [
'uri' => 'something/:something/:some_param/:something_else',
'replacements' => ['::something::', '::some_param::', '::something else::'],
'expected' => 'something/:something/:some_param/:something_else',
],
'Minor and major params in weird order' => [
'uri' => 'something/:something/:guild_id/:something_else/:channel_id',
'replacements' => ['::something::', '::guild id::', '::something else::', '::channel id::'],
'expected' => 'something/:something/::guild id::/:something_else/::channel id::',
],
];
}
/**
* @dataProvider allParamProvider
*/
public function testBindAllParams(string $uri, array $replacements, string $expected)
{
$endpoint = new Endpoint($uri);
$endpoint->bindArgs(...$replacements);
$this->assertEquals(
$expected,
$endpoint->toAbsoluteEndpoint()
);
}
public function allParamProvider(): array
{
return [
'Several major params' => [
'uri' => 'something/:guild_id/:channel_id/:webhook_id',
'replacements' => ['::guild id::', '::channel id::', '::webhook id::'],
'expected' => 'something/::guild id::/::channel id::/::webhook id::',
],
'Single major param' => [
'uri' => 'something/:guild_id',
'replacements' => ['::guild id::'],
'expected' => 'something/::guild id::',
],
'Single major param, some minor params' => [
'uri' => 'something/:guild_id/:some_param/:something_else',
'replacements' => ['::guild id::', '::some param::', '::something else::'],
'expected' => 'something/::guild id::/::some param::/::something else::',
],
'Only minor params' => [
'uri' => 'something/:something/:some_param/:other',
'replacements' => ['::something::', '::some param::', '::something else::'],
'expected' => 'something/::something::/::some param::/::something else::',
],
'Minor and major params in weird order' => [
'uri' => 'something/:something/:guild_id/:other/:channel_id',
'replacements' => ['::something::', '::guild id::', '::something else::', '::channel id::'],
'expected' => 'something/::something::/::guild id::/::something else::/::channel id::',
],
// @see https://github.com/discord-php/DiscordPHP-Http/issues/16
// 'Params with same prefix, short first' => [
// 'uri' => 'something/:thing/:thing_other',
// 'replacements' => ['::thing::', '::thing other::'],
// 'expected' => 'something/::thing::/::thing other::',
// ],
// 'Params with same prefix, short first' => [
// 'uri' => 'something/:thing_other/:thing',
// 'replacements' => ['::thing other::', '::thing::'],
// 'expected' => 'something/::thing other::/::thing::',
// ],
];
}
public function testBindAssoc()
{
$endpoint = new Endpoint('something/:first/:second');
$endpoint->bindAssoc([
'second' => '::second::',
'first' => '::first::',
]);
$this->assertEquals(
'something/::first::/::second::',
$endpoint->toAbsoluteEndpoint()
);
}
public function testItConvertsToString()
{
$this->assertEquals(
'something/::first::/::second::',
(string) Endpoint::bind(
'something/:first/:second',
'::first::',
'::second::'
)
);
}
public function itCanAddQueryParams()
{
$endpoint = new Endpoint('something/:param');
$endpoint->bindArgs('param');
$endpoint->addQuery('something', 'value');
$endpoint->addQuery('boolval', true);
$this->assertEquals(
'something/param?something=value&boolval=1',
$endpoint->toAbsoluteEndpoint()
);
}
public function itDoesNotAddQueryParamsForMajorParameters()
{
$endpoint = new Endpoint('something/:guild_id');
$endpoint->bindArgs('param');
$endpoint->addQuery('something', 'value');
$endpoint->addQuery('boolval', true);
$this->assertEquals(
'something/param',
$endpoint->toAbsoluteEndpoint(true)
);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http\Multipart;
use Discord\Http\Multipart\MultipartBody;
use Discord\Http\Multipart\MultipartField;
use Mockery;
use PHPUnit\Framework\TestCase;
class MultipartTest extends TestCase
{
/**
* @dataProvider multipartFieldStringConversionProvider
*/
public function testMultipartFieldStringConversion(array $constructorArgs, string $expected)
{
$multipartField = new MultipartField(...$constructorArgs);
$this->assertEquals($expected, (string) $multipartField);
}
public function multipartFieldStringConversionProvider(): array
{
return [
'Completely filled' => [
'args' => [
'::name::',
'::content::',
[
'Header-Name' => 'Value',
],
'::filename::',
],
'expected' => <<<EXPECTED
Content-Disposition: form-data; name="::name::"; filename="%3A%3Afilename%3A%3A"
Header-Name: Value
::content::
EXPECTED
],
'Missing filename' => [
'args' => [
'::name::',
'::content::',
[
'Header-Name' => 'Value',
],
null,
],
'expected' => <<<EXPECTED
Content-Disposition: form-data; name="::name::"
Header-Name: Value
::content::
EXPECTED
],
'No headers' => [
'args' => [
'::name::',
'::content::',
[],
'::filename::',
],
'expected' => <<<EXPECTED
Content-Disposition: form-data; name="::name::"; filename="%3A%3Afilename%3A%3A"
::content::
EXPECTED
],
];
}
public function testMultipartBodyBuilding()
{
$fields = array_map(function (string $return) {
$mock = Mockery::mock(MultipartField::class);
$mock->shouldReceive('__toString')->andReturn($return);
return $mock;
}, ['::first field::', '::second field::', '::third field::']);
$multipartBody = new MultipartBody($fields, '::boundary::');
$this->assertEquals(
<<<EXPECTED
--::boundary::
::first field::
--::boundary::
::second field::
--::boundary::
::third field::
--::boundary::--
EXPECTED,
(string) $multipartBody
);
$this->assertEquals([
'Content-Type' => 'multipart/form-data; boundary=::boundary::',
'Content-Length' => strlen((string) $multipartBody),
], $multipartBody->getHeaders());
}
public function testGeneratingBoundary()
{
$multipartBody = new MultipartBody([
Mockery::mock(MultipartField::class),
]);
$this->assertNotNull($multipartBody->boundary);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http;
use Discord\Http\Endpoint;
use Discord\Http\Http;
use Discord\Http\Request;
use Mockery;
use PHPUnit\Framework\TestCase;
use React\Promise\Deferred;
class RequestTest extends TestCase
{
private function getRequest(
?Deferred $deferred = null,
string $method = '',
?Endpoint $url = null,
string $content = '',
array $headers = []
) {
$url = $url ?? new Endpoint('');
$deferred = $deferred ?? new Deferred();
return new Request(
$deferred,
$method,
$url,
$content,
$headers
);
}
public function testGetDeferred()
{
$deferred = Mockery::mock(Deferred::class);
$request = $this->getRequest($deferred);
$this->assertEquals($deferred, $request->getDeferred());
}
public function testGetMethod()
{
$request = $this->getRequest(null, '::method::');
$this->assertEquals('::method::', $request->getMethod());
}
public function testGetUrl()
{
$request = $this->getRequest(null, '', new Endpoint('::url::'));
$this->assertEquals(Http::BASE_URL.'/::url::', $request->getUrl());
}
public function testGetContent()
{
$request = $this->getRequest(null, '', null, '::content::');
$this->assertEquals('::content::', $request->getContent());
}
public function testGetHeaders()
{
$request = $this->getRequest(null, '', null, '::content::', ['something' => 'value']);
$this->assertEquals(['something' => 'value'], $request->getHeaders());
}
public function testGetBucketId()
{
$endpoint = Mockery::mock(Endpoint::class);
$endpoint->shouldReceive('toAbsoluteEndpoint')->andReturn('::endpoint::');
$request = $this->getRequest(null, '::method::', $endpoint);
$this->assertEquals('::method::::endpoint::', $request->getBucketID());
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http\Drivers;
use Discord\Http\DriverInterface;
use Discord\Http\Drivers\Guzzle;
use React\EventLoop\Loop;
use Tests\Discord\Http\DriverInterfaceTest;
class GuzzleTest extends DriverInterfaceTest
{
protected function getDriver(): DriverInterface
{
return new Guzzle(Loop::get());
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
namespace Tests\Discord\Http\Drivers;
use Discord\Http\DriverInterface;
use Discord\Http\Drivers\React;
use React\EventLoop\Loop;
use Tests\Discord\Http\DriverInterfaceTest;
class ReactTest extends DriverInterfaceTest
{
protected function getDriver(): DriverInterface
{
return new React(Loop::get());
}
}

View File

@@ -0,0 +1,32 @@
<?php
/*
* This file is a part of the DiscordPHP-Http project.
*
* Copyright (c) 2021-present David Cole <david.cole1340@gmail.com>
*
* This file is subject to the MIT license that is bundled
* with this source code in the LICENSE file.
*/
use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
require __DIR__.'/../../vendor/autoload.php';
$http = new HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) {
$response = [
'method' => $request->getMethod(),
'args' => $request->getQueryParams(),
'json' => $request->getHeader('Content-Type') === ['application/json']
? json_decode($request->getBody())
: [],
];
return Response::json($response);
});
$socket = new SocketServer('127.0.0.1:8888');
$http->listen($socket);