Commit inicial con archivos existentes

This commit is contained in:
2026-01-17 16:14:00 -06:00
parent 48671dc88e
commit 4c48c279de
2539 changed files with 2412708 additions and 0 deletions

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

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

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.2
## 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

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

@@ -0,0 +1,32 @@
{
"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"
}
},
"require": {
"php": "^7.2|^8.0",
"react/http": "^1.2",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"react/promise": "^2.2"
},
"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"
}
}

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

@@ -0,0 +1,215 @@
<?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 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;
/**
* 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;
}
/**
* 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;
}
$checkQueue = function () use (&$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();
($this->runRequest)($request)->done(function (ResponseInterface $response) use (&$checkQueue) {
$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
$checkQueue();
}, function ($rateLimit) use (&$checkQueue, $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(), $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 {
$checkQueue();
}
});
};
$this->checkerRunning = true;
$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\ExtendedPromiseInterface;
/**
* 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 ExtendedPromiseInterface<ResponseInterface>
*/
public function runRequest(Request $request): ExtendedPromiseInterface;
}

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\ExtendedPromiseInterface;
/**
* 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): ExtendedPromiseInterface
{
// 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,65 @@
<?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\ExtendedPromiseInterface;
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 Guzzle 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);
}
public function runRequest(Request $request): ExtendedPromiseInterface
{
return $this->browser->{$request->getMethod()}(
$request->getUrl(),
$request->getHeaders(),
$request->getContent()
);
}
}

View File

@@ -0,0 +1,413 @@
<?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
{
// GET
public const GATEWAY = 'gateway';
// GET
public const GATEWAY_BOT = self::GATEWAY.'/bot';
// GET, POST, PUT
public const GLOBAL_APPLICATION_COMMANDS = 'applications/:application_id/commands';
// GET, PATCH, DELETE
public const GLOBAL_APPLICATION_COMMAND = 'applications/:application_id/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 = 'applications/:application_id/guilds/:guild_id/commands/permissions';
// GET, PATCH, DELETE
public const GUILD_APPLICATION_COMMAND = 'applications/:application_id/guilds/:guild_id/commands/:command_id';
// GET, PUT
public const GUILD_APPLICATION_COMMAND_PERMISSIONS = 'applications/:application_id/guilds/:guild_id/commands/:command_id/permissions';
// POST
public const INTERACTION_RESPONSE = 'interactions/:interaction_id/:interaction_token/callback';
// PATCH, DELETE
public const ORIGINAL_INTERACTION_RESPONSE = 'webhooks/:application_id/:interaction_token/messages/@original';
// POST
public const CREATE_INTERACTION_FOLLOW_UP = 'webhooks/:application_id/:interaction_token';
// PATCH, DELETE
public const INTERACTION_FOLLOW_UP = 'webhooks/:application_id/:interaction_token/messages/:message_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
public const CHANNEL_PINS = self::CHANNEL.'/pins';
// PUT, DELETE
public const CHANNEL_PIN = self::CHANNEL.'/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_ACTIVE = self::CHANNEL_THREADS.'/active';
// 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';
// 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, 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_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';
// GET, PATCH
public const GUILD_ROLES = self::GUILD.'/roles';
// 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';
// PATCH
public const GUILD_USER_CURRENT_VOICE_STATE = self::GUILD.'/voice-states/@me';
// 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, 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';
// 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.'/guilds/:guild_id/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
public const APPLICATION_CURRENT = 'oauth2/applications/@me';
// 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] ?? [];
}
}
/**
* 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 = (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;
foreach ($this->vars as $var) {
if (! isset($this->args[$var]) || ($onlyMajorParameters && ! $this->isMajorParameter($var))) {
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, self::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 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,21 @@
<?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
{
}

View File

@@ -0,0 +1,21 @@
<?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
{
}

View File

@@ -0,0 +1,21 @@
<?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
{
}

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 Exception;
/**
* Thrown when a request to Discord's REST API fails.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class RequestFailedException extends Exception
{
}

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

@@ -0,0 +1,513 @@
<?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\ContentTooLongException;
use Discord\Http\Exceptions\InvalidTokenException;
use Discord\Http\Exceptions\NoPermissionsException;
use Discord\Http\Exceptions\NotFoundException;
use Discord\Http\Exceptions\RequestFailedException;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\ExtendedPromiseInterface;
use RuntimeException;
use SplQueue;
use Throwable;
/**
* Discord HTTP client.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class Http
{
/**
* DiscordPHP-Http version.
*
* @var string
*/
public const VERSION = 'v9.1.9';
/**
* Current Discord HTTP API version.
*
* @var string
*/
public const HTTP_API_VERSION = 9;
/**
* 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;
/**
* Number of requests that are waiting for a response.
*
* @var int
*/
protected $waiting = 0;
/**
* 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;
}
/**
* 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 ExtendedPromiseInterface
*/
public function get($url, $content = null, array $headers = []): ExtendedPromiseInterface
{
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 ExtendedPromiseInterface
*/
public function post($url, $content = null, array $headers = []): ExtendedPromiseInterface
{
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 ExtendedPromiseInterface
*/
public function put($url, $content = null, array $headers = []): ExtendedPromiseInterface
{
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 ExtendedPromiseInterface
*/
public function patch($url, $content = null, array $headers = []): ExtendedPromiseInterface
{
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 ExtendedPromiseInterface
*/
public function delete($url, $content = null, array $headers = []): ExtendedPromiseInterface
{
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 ExtendedPromiseInterface
*/
public function queueRequest(string $method, Endpoint $url, $content, array $headers = []): ExtendedPromiseInterface
{
$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 there is content and Content-Type is not set,
// assume it is JSON.
if (! is_null($content) && ! isset($headers['Content-Type'])) {
$content = json_encode($content);
$baseHeaders['Content-Type'] = 'application/json';
$baseHeaders['Content-Length'] = strlen($content);
}
$headers = array_merge($baseHeaders, $headers);
$request = new Request($deferred, $method, $url, $content ?? '', $headers);
$this->sortIntoBucket($request);
return $deferred->promise();
}
/**
* Executes a request.
*
* @param Request $request
* @param Deferred $deferred
*
* @return ExtendedPromiseInterface
*/
protected function executeRequest(Request $request, Deferred $deferred = null): ExtendedPromiseInterface
{
if ($deferred === null) {
$deferred = new Deferred();
}
if ($this->rateLimit) {
$deferred->reject($this->rateLimit);
return $deferred->promise();
}
$this->driver->runRequest($request)->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 RuntimeException('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 RuntimeException('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();
$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(): void
{
if ($this->waiting >= static::CONCURRENT_REQUESTS || $this->queue->isEmpty()) {
$this->logger->debug('http not checking', ['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();
$deferred->resolve($result);
}, function ($e) use ($deferred) {
--$this->waiting;
$this->checkQueue();
$deferred->reject($e);
});
}
/**
* 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 (isset($content->code)) {
$errorCode = $content->code;
}
$reason .= json_encode($content, JSON_PRETTY_PRINT);
} else {
$reason .= $errorBody;
}
switch ($response->getStatusCode()) {
case 401:
return new InvalidTokenException($reason, $errorCode);
case 403:
return new NoPermissionsException($reason, $errorCode);
case 404:
return new NotFoundException($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, '.self::VERSION.')';
}
}

View File

@@ -0,0 +1,76 @@
<?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;
/**
* Represents a rate-limit given by Discord.
*
* @author David Cole <david.cole1340@gmail.com>
*/
class RateLimit
{
/**
* 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;
}
}