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:
parent
cecb7d64ff
commit
d4c751e01a
11 changed files with 1152 additions and 0 deletions
17
composer.json
Normal file
17
composer.json
Normal 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
106
src/Discord/Auth.php
Normal 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
58
src/Discord/Delete.php
Normal 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
123
src/Discord/Post.php
Normal 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
92
src/Slack/Auth.php
Normal 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
120
src/Slack/Post.php
Normal 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
122
src/Telegram/Auth.php
Normal 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
113
src/Telegram/Chats.php
Normal 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
64
src/Telegram/Delete.php
Normal 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
212
src/Telegram/Post.php
Normal 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
125
src/Telegram/Read.php
Normal 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];
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue