feat: extract chat providers from app/Plug/Chat

Discord, Slack, Telegram providers
with Core\Plug\Chat namespace alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-09 17:28:14 +00:00
parent cecb7d64ff
commit d4c751e01a
11 changed files with 1152 additions and 0 deletions

17
composer.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "core/php-plug-chat",
"description": "Chat platform integrations for the Plug framework",
"type": "library",
"license": "EUPL-1.2",
"require": {
"php": "^8.2",
"core/php": "^1.0"
},
"autoload": {
"psr-4": {
"Core\\Plug\\Chat\\": "src/"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

106
src/Discord/Auth.php Normal file
View file

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Discord;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Authenticable;
use Core\Plug\Response;
/**
* Discord webhook authentication.
*
* Uses webhooks for posting - no OAuth needed.
*/
class Auth implements Authenticable
{
use BuildsResponse;
use UsesHttp;
private ?string $webhookUrl = null;
public static function identifier(): string
{
return 'discord';
}
public static function name(): string
{
return 'Discord';
}
/**
* Set webhook URL for validation.
*/
public function withWebhook(string $webhookUrl): self
{
$this->webhookUrl = $webhookUrl;
return $this;
}
/**
* Discord uses webhook URLs, not OAuth.
*/
public function getAuthUrl(): string
{
return 'https://discord.com/developers/applications';
}
/**
* Validate webhook URL and return credentials.
*
* @param array $params ['webhook_url' => string, 'channel_name' => string]
*/
public function requestAccessToken(array $params): array
{
$webhookUrl = $params['webhook_url'] ?? $this->webhookUrl;
$channelName = $params['channel_name'] ?? 'Discord Channel';
if (! $webhookUrl) {
return ['error' => 'Webhook URL is required'];
}
// Validate webhook URL format
if (! str_starts_with($webhookUrl, 'https://discord.com/api/webhooks/')) {
return ['error' => 'Invalid Discord webhook URL'];
}
// Verify webhook by fetching its info
$response = $this->http()->get($webhookUrl);
if (! $response->successful()) {
return ['error' => 'Invalid or expired webhook URL'];
}
$data = $response->json();
return [
'webhook_url' => $webhookUrl,
'webhook_id' => $data['id'] ?? null,
'channel_id' => $data['channel_id'] ?? null,
'guild_id' => $data['guild_id'] ?? null,
'channel_name' => $data['name'] ?? $channelName,
'account_id' => $data['id'] ?? md5($webhookUrl),
];
}
public function getAccount(): Response
{
if (! $this->webhookUrl) {
return $this->error('Webhook URL is required');
}
$response = $this->http()->get($this->webhookUrl);
return $this->fromHttp($response, fn ($data) => [
'id' => $data['id'],
'name' => $data['name'],
'channel_id' => $data['channel_id'],
'guild_id' => $data['guild_id'] ?? null,
'avatar' => $data['avatar'] ? "https://cdn.discordapp.com/avatars/{$data['id']}/{$data['avatar']}.png" : null,
]);
}
}

58
src/Discord/Delete.php Normal file
View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Discord;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Deletable;
use Core\Plug\Response;
/**
* Discord message deletion via webhooks.
*/
class Delete implements Deletable
{
use BuildsResponse;
use UsesHttp;
private string $webhookUrl = '';
/**
* Set webhook URL.
*/
public function withWebhook(string $webhookUrl): self
{
$this->webhookUrl = $webhookUrl;
return $this;
}
/**
* Delete a message posted via this webhook.
*
* @param string $id Message ID
*/
public function delete(string $id): Response
{
if (! $this->webhookUrl) {
return $this->error('Webhook URL is required');
}
$response = $this->http()->delete("{$this->webhookUrl}/messages/{$id}");
// Discord returns 204 No Content on success
if ($response->status() === 204) {
return $this->ok([
'deleted' => true,
'id' => $id,
]);
}
return $this->fromHttp($response, fn ($data) => [
'deleted' => false,
'error' => $data['message'] ?? 'Failed to delete message',
]);
}
}

123
src/Discord/Post.php Normal file
View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Discord;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Postable;
use Core\Plug\Response;
use Illuminate\Support\Collection;
/**
* Discord message posting via webhooks.
*/
class Post implements Postable
{
use BuildsResponse;
use UsesHttp;
private string $webhookUrl = '';
/**
* Set webhook URL.
*/
public function withWebhook(string $webhookUrl): self
{
$this->webhookUrl = $webhookUrl;
return $this;
}
/**
* Post a message to Discord.
*
* @param string $text Message content
* @param Collection $media Images to embed
* @param array $params username, avatar_url, tts, embed options
*/
public function publish(string $text, Collection $media, array $params = []): Response
{
if (! $this->webhookUrl) {
return $this->error('Webhook URL is required');
}
$payload = [];
if ($text) {
$payload['content'] = $text;
}
// Handle embeds for media
if ($media->isNotEmpty()) {
$embeds = [];
foreach ($media as $item) {
$imageUrl = $item['url'] ?? $item['path'] ?? null;
if ($imageUrl) {
$embed = [
'image' => ['url' => $imageUrl],
];
// Add title/description if provided
if (isset($item['title'])) {
$embed['title'] = $item['title'];
}
if (isset($item['description'])) {
$embed['description'] = $item['description'];
}
$embeds[] = $embed;
}
}
if (! empty($embeds)) {
$payload['embeds'] = $embeds;
}
}
// Custom embed from params
if (isset($params['embed'])) {
$payload['embeds'] = array_merge($payload['embeds'] ?? [], [$params['embed']]);
}
// Optional customisation
if (isset($params['username'])) {
$payload['username'] = $params['username'];
}
if (isset($params['avatar_url'])) {
$payload['avatar_url'] = $params['avatar_url'];
}
if (isset($params['tts'])) {
$payload['tts'] = (bool) $params['tts'];
}
// Use ?wait=true to get message ID in response
$response = $this->http()->post($this->webhookUrl.'?wait=true', $payload);
return $this->fromHttp($response, fn ($data) => [
'id' => $data['id'],
'channel_id' => $data['channel_id'],
'timestamp' => $data['timestamp'] ?? null,
]);
}
/**
* Discord message URL.
*/
public static function externalPostUrl(string $guildId, string $channelId, string $messageId): string
{
return "https://discord.com/channels/{$guildId}/{$channelId}/{$messageId}";
}
/**
* Discord server URL.
*/
public static function externalAccountUrl(string $guildId): string
{
return "https://discord.com/channels/{$guildId}";
}
}

92
src/Slack/Auth.php Normal file
View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Slack;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Authenticable;
use Core\Plug\Response;
/**
* Slack webhook authentication.
*
* Uses incoming webhooks for posting - no OAuth needed.
*/
class Auth implements Authenticable
{
use BuildsResponse;
use UsesHttp;
private ?string $webhookUrl = null;
public static function identifier(): string
{
return 'slack';
}
public static function name(): string
{
return 'Slack';
}
/**
* Set webhook URL for validation.
*/
public function withWebhook(string $webhookUrl): self
{
$this->webhookUrl = $webhookUrl;
return $this;
}
/**
* Slack uses webhook URLs, not OAuth.
*/
public function getAuthUrl(): string
{
return 'https://api.slack.com/apps';
}
/**
* Validate webhook URL and return credentials.
*
* @param array $params ['webhook_url' => string, 'channel_name' => string]
*/
public function requestAccessToken(array $params): array
{
$webhookUrl = $params['webhook_url'] ?? $this->webhookUrl;
$channelName = $params['channel_name'] ?? 'Slack Channel';
if (! $webhookUrl) {
return ['error' => 'Webhook URL is required'];
}
// Validate webhook URL format
if (! str_starts_with($webhookUrl, 'https://hooks.slack.com/')) {
return ['error' => 'Invalid Slack webhook URL'];
}
// Test the webhook with a simple request
$response = $this->http()->post($webhookUrl, [
'text' => '', // Empty test - Slack will accept but not post
]);
// Slack returns 'invalid_payload' for empty text, which confirms webhook is valid
if (! $response->successful() && $response->body() !== 'invalid_payload') {
return ['error' => 'Invalid or expired webhook URL'];
}
return [
'webhook_url' => $webhookUrl,
'channel_name' => $channelName,
'account_id' => md5($webhookUrl),
];
}
public function getAccount(): Response
{
return $this->error('Use webhook credentials directly');
}
}

120
src/Slack/Post.php Normal file
View file

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Slack;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Postable;
use Core\Plug\Response;
use Illuminate\Support\Collection;
/**
* Slack message posting via incoming webhooks.
*/
class Post implements Postable
{
use BuildsResponse;
use UsesHttp;
private string $webhookUrl = '';
/**
* Set webhook URL.
*/
public function withWebhook(string $webhookUrl): self
{
$this->webhookUrl = $webhookUrl;
return $this;
}
/**
* Post a message to Slack.
*
* @param string $text Message text (supports mrkdwn)
* @param Collection $media Images to include
* @param array $params username, icon_emoji, icon_url
*/
public function publish(string $text, Collection $media, array $params = []): Response
{
if (! $this->webhookUrl) {
return $this->error('Webhook URL is required');
}
$blocks = [];
// Add text as section block
if ($text) {
$blocks[] = [
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => $text,
],
];
}
// Add media as image blocks
foreach ($media as $item) {
$imageUrl = $item['url'] ?? $item['path'] ?? null;
if ($imageUrl) {
$blocks[] = [
'type' => 'image',
'image_url' => $imageUrl,
'alt_text' => $item['alt_text'] ?? $item['name'] ?? 'Image',
];
}
}
$payload = [];
if (! empty($blocks)) {
$payload['blocks'] = $blocks;
} else {
$payload['text'] = $text ?: 'Message from Host UK';
}
// Optional customisation
if (isset($params['username'])) {
$payload['username'] = $params['username'];
}
if (isset($params['icon_emoji'])) {
$payload['icon_emoji'] = $params['icon_emoji'];
}
if (isset($params['icon_url'])) {
$payload['icon_url'] = $params['icon_url'];
}
$response = $this->http()->post($this->webhookUrl, $payload);
// Slack webhooks return 'ok' as plain text on success
if ($response->successful() && $response->body() === 'ok') {
return $this->ok([
'id' => uniqid('slack_'),
'success' => true,
]);
}
return $this->error($response->body() ?: 'Failed to post message');
}
/**
* Slack doesn't provide post URLs for webhook messages.
*/
public static function externalPostUrl(string $workspace, string $channel): string
{
return "https://{$workspace}.slack.com/archives/{$channel}";
}
/**
* Slack workspace URL.
*/
public static function externalAccountUrl(string $workspace): string
{
return "https://{$workspace}.slack.com";
}
}

122
src/Telegram/Auth.php Normal file
View file

@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Telegram;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Authenticable;
use Core\Plug\Response;
/**
* Telegram Bot API authentication.
*
* Uses bot tokens for authentication.
*/
class Auth implements Authenticable
{
use BuildsResponse;
use UsesHttp;
private const API_URL = 'https://api.telegram.org';
private ?string $botToken = null;
public static function identifier(): string
{
return 'telegram';
}
public static function name(): string
{
return 'Telegram';
}
/**
* Set bot token for validation.
*/
public function withBotToken(string $botToken): self
{
$this->botToken = $botToken;
return $this;
}
/**
* BotFather link for creating bots.
*/
public function getAuthUrl(): string
{
return 'https://t.me/BotFather';
}
/**
* Validate bot token and return credentials.
*
* @param array $params ['bot_token' => string, 'chat_id' => string]
*/
public function requestAccessToken(array $params): array
{
$botToken = $params['bot_token'] ?? $this->botToken;
$chatId = $params['chat_id'] ?? '';
if (! $botToken) {
return ['error' => 'Bot token is required'];
}
// Verify the bot token
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getMe");
if (! $response->successful()) {
return ['error' => 'Invalid bot token'];
}
$data = $response->json();
if (! ($data['ok'] ?? false)) {
return ['error' => $data['description'] ?? 'Invalid bot token'];
}
$bot = $data['result'];
return [
'access_token' => $botToken,
'chat_id' => $chatId,
'bot_id' => (string) $bot['id'],
'bot_username' => $bot['username'],
'bot_name' => $bot['first_name'],
'account_id' => (string) $bot['id'],
];
}
public function getAccount(): Response
{
if (! $this->botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$this->botToken}/getMe");
return $this->fromHttp($response, function ($data) {
$bot = $data['result'] ?? $data;
return [
'id' => (string) $bot['id'],
'name' => $bot['first_name'],
'username' => $bot['username'],
'can_join_groups' => $bot['can_join_groups'] ?? false,
'can_read_messages' => $bot['can_read_all_group_messages'] ?? false,
'supports_inline' => $bot['supports_inline_queries'] ?? false,
];
});
}
/**
* Bot profile URL.
*/
public static function externalAccountUrl(string $username): string
{
return "https://t.me/{$username}";
}
}

113
src/Telegram/Chats.php Normal file
View file

@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Telegram;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\ManagesTokens;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Listable;
use Core\Plug\Response;
/**
* Telegram chat listing.
*/
class Chats implements Listable
{
use BuildsResponse;
use ManagesTokens;
use UsesHttp;
private const API_URL = 'https://api.telegram.org';
/**
* List chats the bot has interacted with.
*
* Note: Telegram bots can only see chats they've received messages from.
*/
public function listEntities(): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
// Get updates to find chats the bot is in
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getUpdates", [
'allowed_updates' => ['message', 'channel_post', 'my_chat_member'],
'limit' => 100,
]);
return $this->fromHttp($response, function ($data) {
$chats = [];
$seenIds = [];
foreach ($data['result'] ?? [] as $update) {
// Try multiple sources for chat info
$chat = $update['message']['chat']
?? $update['channel_post']['chat']
?? $update['my_chat_member']['chat']
?? null;
if ($chat && ! in_array($chat['id'], $seenIds)) {
$seenIds[] = $chat['id'];
$chats[] = [
'id' => (string) $chat['id'],
'name' => $chat['title'] ?? $chat['first_name'] ?? $chat['username'] ?? 'Unknown',
'type' => $chat['type'] ?? 'private',
'username' => $chat['username'] ?? null,
];
}
}
return ['chats' => $chats];
});
}
/**
* Get chat member count.
*/
public function memberCount(string $chatId): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getChatMemberCount", [
'chat_id' => $chatId,
]);
return $this->fromHttp($response, fn ($data) => [
'count' => $data['result'] ?? 0,
]);
}
/**
* Get chat administrators.
*/
public function administrators(string $chatId): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getChatAdministrators", [
'chat_id' => $chatId,
]);
return $this->fromHttp($response, function ($data) {
return [
'administrators' => array_map(fn ($admin) => [
'id' => (string) $admin['user']['id'],
'username' => $admin['user']['username'] ?? null,
'name' => $admin['user']['first_name'] ?? '',
'status' => $admin['status'],
'is_anonymous' => $admin['is_anonymous'] ?? false,
], $data['result'] ?? []),
];
});
}
}

64
src/Telegram/Delete.php Normal file
View file

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Telegram;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\ManagesTokens;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Deletable;
use Core\Plug\Response;
/**
* Telegram message deletion via Bot API.
*/
class Delete implements Deletable
{
use BuildsResponse;
use ManagesTokens;
use UsesHttp;
private const API_URL = 'https://api.telegram.org';
private ?string $chatId = null;
/**
* Set chat ID for deletion.
*/
public function inChat(string $chatId): self
{
$this->chatId = $chatId;
return $this;
}
/**
* Delete a message.
*
* @param string $id Message ID
*/
public function delete(string $id): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
if (! $this->chatId) {
return $this->error('Chat ID is required');
}
$response = $this->http()->post(self::API_URL."/bot{$botToken}/deleteMessage", [
'chat_id' => $this->chatId,
'message_id' => $id,
]);
return $this->fromHttp($response, function ($data) use ($id) {
return [
'deleted' => $data['result'] ?? $data['ok'] ?? true,
'id' => $id,
];
});
}
}

212
src/Telegram/Post.php Normal file
View file

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Telegram;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\ManagesTokens;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Postable;
use Core\Plug\Response;
use Illuminate\Support\Collection;
/**
* Telegram message posting via Bot API.
*/
class Post implements Postable
{
use BuildsResponse;
use ManagesTokens;
use UsesHttp;
private const API_URL = 'https://api.telegram.org';
private ?string $chatId = null;
/**
* Set default chat ID.
*/
public function toChatId(string $chatId): self
{
$this->chatId = $chatId;
return $this;
}
/**
* Send a message to Telegram.
*
* @param string $text Message text
* @param Collection $media Media items to send
* @param array $params chat_id, parse_mode, disable_web_page_preview, disable_notification
*/
public function publish(string $text, Collection $media, array $params = []): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$chatId = $params['chat_id'] ?? $this->chatId;
if (! $chatId) {
return $this->error('Chat ID is required');
}
// Handle media
if ($media->isNotEmpty()) {
return $this->sendMedia($botToken, $chatId, $text, $media, $params);
}
// Send text message
$messageData = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => $params['parse_mode'] ?? 'HTML',
];
if (isset($params['disable_web_page_preview'])) {
$messageData['disable_web_page_preview'] = $params['disable_web_page_preview'];
}
if (isset($params['disable_notification'])) {
$messageData['disable_notification'] = $params['disable_notification'];
}
if (isset($params['reply_to_message_id'])) {
$messageData['reply_to_message_id'] = $params['reply_to_message_id'];
}
$response = $this->http()->post(self::API_URL."/bot{$botToken}/sendMessage", $messageData);
return $this->fromHttp($response, function ($data) {
$result = $data['result'] ?? $data;
return [
'id' => (string) $result['message_id'],
'chat_id' => (string) ($result['chat']['id'] ?? ''),
];
});
}
/**
* Send media (photo, video, or media group).
*/
private function sendMedia(string $botToken, string $chatId, string $text, Collection $media, array $params): Response
{
$mediaItems = [];
foreach ($media as $index => $item) {
$type = $this->getMediaType($item);
$mediaItem = [
'type' => $type,
'media' => $item['url'] ?? $item['path'] ?? '',
];
// Only add caption to first item
if ($index === 0 && $text) {
$mediaItem['caption'] = $text;
$mediaItem['parse_mode'] = $params['parse_mode'] ?? 'HTML';
}
$mediaItems[] = $mediaItem;
}
// Single media item - use specific method
if (count($mediaItems) === 1) {
return $this->sendSingleMedia($botToken, $chatId, $mediaItems[0], $params);
}
// Multiple media items - use sendMediaGroup
$response = $this->http()->post(self::API_URL."/bot{$botToken}/sendMediaGroup", [
'chat_id' => $chatId,
'media' => json_encode($mediaItems),
'disable_notification' => $params['disable_notification'] ?? false,
]);
return $this->fromHttp($response, function ($data) {
$results = $data['result'] ?? [];
$first = $results[0] ?? $data;
return [
'id' => (string) ($first['message_id'] ?? ''),
'chat_id' => (string) ($first['chat']['id'] ?? ''),
'message_count' => count($results),
];
});
}
/**
* Send single media item.
*/
private function sendSingleMedia(string $botToken, string $chatId, array $item, array $params): Response
{
$method = match ($item['type']) {
'photo' => 'sendPhoto',
'video' => 'sendVideo',
'audio' => 'sendAudio',
'document' => 'sendDocument',
'animation' => 'sendAnimation',
default => 'sendPhoto',
};
$payload = [
'chat_id' => $chatId,
$item['type'] => $item['media'],
];
if (isset($item['caption'])) {
$payload['caption'] = $item['caption'];
$payload['parse_mode'] = $item['parse_mode'] ?? 'HTML';
}
if (isset($params['disable_notification'])) {
$payload['disable_notification'] = $params['disable_notification'];
}
$response = $this->http()->post(self::API_URL."/bot{$botToken}/{$method}", $payload);
return $this->fromHttp($response, function ($data) {
$result = $data['result'] ?? $data;
return [
'id' => (string) $result['message_id'],
'chat_id' => (string) ($result['chat']['id'] ?? ''),
];
});
}
/**
* Determine media type from item.
*/
private function getMediaType(array $item): string
{
$mimeType = $item['mime_type'] ?? '';
if (str_starts_with($mimeType, 'image/gif')) {
return 'animation';
}
if (str_starts_with($mimeType, 'image/')) {
return 'photo';
}
if (str_starts_with($mimeType, 'video/')) {
return 'video';
}
if (str_starts_with($mimeType, 'audio/')) {
return 'audio';
}
return 'document';
}
/**
* Telegram message URL.
*/
public static function externalPostUrl(string $chatUsername, string $messageId): string
{
return "https://t.me/{$chatUsername}/{$messageId}";
}
}

125
src/Telegram/Read.php Normal file
View file

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Core\Plug\Chat\Telegram;
use Core\Plug\Concern\BuildsResponse;
use Core\Plug\Concern\ManagesTokens;
use Core\Plug\Concern\UsesHttp;
use Core\Plug\Contract\Readable;
use Core\Plug\Response;
/**
* Telegram bot and chat information.
*/
class Read implements Readable
{
use BuildsResponse;
use ManagesTokens;
use UsesHttp;
private const API_URL = 'https://api.telegram.org';
/**
* Get bot information.
*
* @param string $id Not used - returns current bot info
*/
public function get(string $id): Response
{
return $this->me();
}
/**
* Get current bot information.
*/
public function me(): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getMe");
return $this->fromHttp($response, function ($data) {
$bot = $data['result'] ?? $data;
return [
'id' => (string) $bot['id'],
'username' => $bot['username'],
'name' => $bot['first_name'],
'can_join_groups' => $bot['can_join_groups'] ?? false,
'can_read_messages' => $bot['can_read_all_group_messages'] ?? false,
'supports_inline' => $bot['supports_inline_queries'] ?? false,
];
});
}
/**
* Get chat information.
*/
public function chat(string $chatId): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getChat", [
'chat_id' => $chatId,
]);
return $this->fromHttp($response, function ($data) {
$chat = $data['result'] ?? $data;
return [
'id' => (string) $chat['id'],
'type' => $chat['type'],
'title' => $chat['title'] ?? null,
'username' => $chat['username'] ?? null,
'first_name' => $chat['first_name'] ?? null,
'description' => $chat['description'] ?? null,
'photo' => $chat['photo']['big_file_id'] ?? null,
];
});
}
/**
* List recent chats (from updates).
*/
public function list(array $params = []): Response
{
$botToken = $this->accessToken();
if (! $botToken) {
return $this->error('Bot token is required');
}
$response = $this->http()->get(self::API_URL."/bot{$botToken}/getUpdates", [
'allowed_updates' => ['message', 'channel_post'],
'limit' => $params['limit'] ?? 100,
]);
return $this->fromHttp($response, function ($data) {
$chats = [];
$seenIds = [];
foreach ($data['result'] ?? [] as $update) {
$chat = $update['message']['chat'] ?? $update['channel_post']['chat'] ?? null;
if ($chat && ! in_array($chat['id'], $seenIds)) {
$seenIds[] = $chat['id'];
$chats[] = [
'id' => (string) $chat['id'],
'name' => $chat['title'] ?? $chat['first_name'] ?? $chat['username'] ?? '',
'type' => $chat['type'] ?? 'private',
'username' => $chat['username'] ?? null,
];
}
}
return ['chats' => $chats];
});
}
}