mirror of
https://github.com/BushlanovDev/max-bot-api-client-php.git
synced 2026-08-24 14:16:49 +00:00
first commit
This commit is contained in:
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
|
||||
|
||||
/**
|
||||
* The main entry point for interacting with the Max Bot API.
|
||||
* This class provides a clean, object-oriented interface over the raw HTTP API.
|
||||
*
|
||||
* @see https://dev.max.ru
|
||||
*/
|
||||
class Api
|
||||
{
|
||||
private const string METHOD_GET = 'GET';
|
||||
|
||||
// private const string METHOD_POST = 'POST';
|
||||
|
||||
private const string ACTION_ME = '/me';
|
||||
|
||||
private readonly ClientApiInterface $client;
|
||||
|
||||
private readonly ModelFactory $modelFactory;
|
||||
|
||||
/**
|
||||
* Api constructor.
|
||||
*
|
||||
* @param string $accessToken Your bot's access token from @MasterBot.
|
||||
* @param ClientApiInterface|null $client Http api client.
|
||||
* @param ModelFactory|null $modelFactory
|
||||
*/
|
||||
public function __construct(string $accessToken, ?ClientApiInterface $client = null, ?ModelFactory $modelFactory = null)
|
||||
{
|
||||
$this->client = $client ?? new Client(
|
||||
$accessToken,
|
||||
new \GuzzleHttp\Client(),
|
||||
new \GuzzleHttp\Psr7\HttpFactory(),
|
||||
new \GuzzleHttp\Psr7\HttpFactory(),
|
||||
);
|
||||
$this->modelFactory = $modelFactory ?? new ModelFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about the current bot, identified by an access token.
|
||||
*
|
||||
* @return BotInfo
|
||||
*/
|
||||
public function getBotInfo(): BotInfo
|
||||
{
|
||||
return $this->modelFactory->createBotInfo(
|
||||
$this->client->request(self::METHOD_GET, self::ACTION_ME)
|
||||
);
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ForbiddenException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\NotFoundException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\UnauthorizedException;
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
|
||||
/**
|
||||
* The low-level HTTP client responsible for communicating with the Max Bot API.
|
||||
* It handles request signing, error handling, and JSON serialization/deserialization.
|
||||
* This class is an abstraction over any PSR-18 compatible HTTP client.
|
||||
*/
|
||||
final class Client implements ClientApiInterface
|
||||
{
|
||||
private const string API_BASE_URL = 'https://botapi.max.ru';
|
||||
|
||||
private const string DEFAULT_API_VERSION = '0.0.6';
|
||||
|
||||
/**
|
||||
* @param string $accessToken Your bot's access token from @MasterBot.
|
||||
* @param ClientInterface $httpClient A PSR-18 compatible HTTP client (e.g., Guzzle).
|
||||
* @param RequestFactoryInterface $requestFactory A PSR-17 factory for creating requests.
|
||||
* @param StreamFactoryInterface $streamFactory A PSR-17 factory for creating request body streams.
|
||||
* @param string $apiVersion The API version to use for requests.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $accessToken,
|
||||
private readonly ClientInterface $httpClient,
|
||||
private readonly RequestFactoryInterface $requestFactory,
|
||||
private readonly StreamFactoryInterface $streamFactory,
|
||||
private readonly string $apiVersion = self::DEFAULT_API_VERSION,
|
||||
) {
|
||||
if (empty($accessToken)) {
|
||||
throw new InvalidArgumentException('Access token cannot be empty.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function request(string $method, string $uri, array $queryParams = [], array $body = []): array
|
||||
{
|
||||
$queryParams['access_token'] = $this->accessToken;
|
||||
$queryParams['v'] = $this->apiVersion;
|
||||
|
||||
$fullUrl = self::API_BASE_URL . $uri . '?' . http_build_query($queryParams);
|
||||
$request = $this->requestFactory->createRequest($method, $fullUrl);
|
||||
|
||||
if (!empty($body)) {
|
||||
try {
|
||||
$payload = json_encode($body, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new SerializationException('Failed to encode request body to JSON.', 0, $e);
|
||||
}
|
||||
$stream = $this->streamFactory->createStream($payload);
|
||||
$request = $request
|
||||
->withBody($stream)
|
||||
->withHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->sendRequest($request);
|
||||
} catch (ClientExceptionInterface $e) {
|
||||
// This catches network errors, DNS failures, timeouts, etc.
|
||||
throw new NetworkException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
$this->handleErrorResponse($response);
|
||||
|
||||
$responseBody = (string)$response->getBody();
|
||||
|
||||
// Handle successful but empty responses (e.g., from DELETE endpoints)
|
||||
if (empty($responseBody)) {
|
||||
// The API spec often returns {"success": true}, so we can simulate that
|
||||
// for consistency if the body is truly empty.
|
||||
return ['success' => true];
|
||||
}
|
||||
|
||||
try {
|
||||
return json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
throw new SerializationException('Failed to decode API response JSON.', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the response for an error status code and throws a corresponding typed exception.
|
||||
*
|
||||
* @throws ClientApiException
|
||||
*/
|
||||
private function handleErrorResponse(ResponseInterface $response): void
|
||||
{
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
// 2xx codes are considered successful.
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
return;
|
||||
}
|
||||
|
||||
$responseBody = (string)$response->getBody();
|
||||
$data = json_decode($responseBody, true) ?? [];
|
||||
$errorCode = $data['code'] ?? 'unknown';
|
||||
$errorMessage = $data['message'] ?? 'An unknown error occurred.';
|
||||
|
||||
$exception = match ($statusCode) {
|
||||
401 => new UnauthorizedException($errorMessage, $errorCode, $response),
|
||||
403 => new ForbiddenException($errorMessage, $errorCode, $response),
|
||||
404 => new NotFoundException($errorMessage, $errorCode, $response),
|
||||
default => new ClientApiException($errorMessage, $errorCode, $response, $statusCode),
|
||||
};
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\ClientApiException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\NetworkException;
|
||||
use BushlanovDev\MaxMessengerBot\Exceptions\SerializationException;
|
||||
|
||||
interface ClientApiInterface
|
||||
{
|
||||
/**
|
||||
* Performs a request to the Max Bot API.
|
||||
*
|
||||
* @param string $method The HTTP method (GET, POST, PATCH, etc.).
|
||||
* @param string $uri The API endpoint (e.g., '/me', '/messages').
|
||||
* @param array<string, mixed> $queryParams Query parameters for the request.
|
||||
* @param array<string, mixed> $body The request body.
|
||||
*
|
||||
* @return array<string, mixed> The decoded JSON response as an associative array.
|
||||
*
|
||||
* @throws ClientApiException for API-level errors (4xx, 5xx).
|
||||
* @throws NetworkException for network-related issues.
|
||||
* @throws SerializationException for JSON encoding/decoding failures.
|
||||
*/
|
||||
public function request(string $method, string $uri, array $queryParams = [], array $body = []): array;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class ClientApiException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
public readonly string $errorCode,
|
||||
public readonly ?ResponseInterface $response = null,
|
||||
public readonly ?int $httpStatusCode = null,
|
||||
?Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $httpStatusCode ?? 0, $previous);
|
||||
}
|
||||
|
||||
public function getHttpStatusCode(): ?int
|
||||
{
|
||||
return $this->httpStatusCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
class ForbiddenException extends ClientApiException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
string $errorCode,
|
||||
?ResponseInterface $response,
|
||||
?Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $errorCode, $response, 403, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class NetworkException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
class NotFoundException extends ClientApiException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
string $errorCode,
|
||||
?ResponseInterface $response,
|
||||
?Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $errorCode, $response, 404, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use LogicException;
|
||||
use Throwable;
|
||||
|
||||
class SerializationException extends LogicException
|
||||
{
|
||||
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Exceptions;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
|
||||
class UnauthorizedException extends ClientApiException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
string $errorCode,
|
||||
?ResponseInterface $response,
|
||||
?Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $errorCode, $response, 401, $previous);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot;
|
||||
|
||||
use BushlanovDev\MaxMessengerBot\Models\BotCommand;
|
||||
use BushlanovDev\MaxMessengerBot\Models\BotInfo;
|
||||
|
||||
/**
|
||||
* Creates DTOs from raw associative arrays returned by the API client.
|
||||
*/
|
||||
class ModelFactory
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*
|
||||
* @return BotInfo
|
||||
*/
|
||||
public function createBotInfo(array $data): BotInfo
|
||||
{
|
||||
$data['commands'] = isset($data['commands']) && is_array($data['commands'])
|
||||
? array_map([BotCommand::class, 'fromArray'], $data['commands']) : null;
|
||||
|
||||
return BotInfo::fromArray($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models;
|
||||
|
||||
abstract readonly class AbstractModel
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
abstract public static function fromArray(array $data): static;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return get_object_vars($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models;
|
||||
|
||||
/**
|
||||
* Command supported by the bot.
|
||||
*/
|
||||
final readonly class BotCommand extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @param string $name Command name (1 to 64 characters)
|
||||
* @param string|null $description Command description (1 to 128 characters)
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $description,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
(string)$data['name'],
|
||||
$data['description'] ? (string)$data['description'] : null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace BushlanovDev\MaxMessengerBot\Models;
|
||||
|
||||
/**
|
||||
* Information about the current bot, which is identified using an access token.
|
||||
*/
|
||||
final readonly class BotInfo extends AbstractModel
|
||||
{
|
||||
/**
|
||||
* @param int $user_id ID user
|
||||
* @param string $first_name User display name
|
||||
* @param string|null $last_name User's display last name
|
||||
* @param string|null $username Unique public name of the user, may be null if the user is not available or no name is set
|
||||
* @param bool $is_bot Is the user a bot
|
||||
* @param int $last_activity_time User last activity time in MAX (Unix time in milliseconds). May be irrelevant if the user has disabled the "online" status in the settings
|
||||
* @param string|null $description User description, may be null if the user has not filled it in (up to 16000 characters)
|
||||
* @param string|null $avatar_url Avatar URL
|
||||
* @param string|null $full_avatar_url Larger Avatar URL
|
||||
* @param BotCommand[]|null $commands Commands supported by the bot (up to 32 elements)
|
||||
*/
|
||||
public function __construct(
|
||||
public int $user_id,
|
||||
public string $first_name,
|
||||
public ?string $last_name,
|
||||
public ?string $username,
|
||||
public bool $is_bot,
|
||||
public int $last_activity_time,
|
||||
public ?string $description,
|
||||
public ?string $avatar_url,
|
||||
public ?string $full_avatar_url,
|
||||
public ?array $commands,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
return new static(
|
||||
(int)$data['user_id'],
|
||||
(string)$data['first_name'],
|
||||
$data['last_name'] ? (string)$data['last_name'] : null,
|
||||
$data['username'] ? (string)$data['username'] : null,
|
||||
(bool)$data['is_bot'],
|
||||
(int)$data['last_activity_time'],
|
||||
$data['description'] ? (string)$data['description'] : null,
|
||||
$data['avatar_url'] ? (string)$data['avatar_url'] : null,
|
||||
$data['full_avatar_url'] ? (string)$data['full_avatar_url'] : null,
|
||||
isset($data['commands']) && is_array($data['commands']) ? $data['commands'] : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user