162 lines
4.9 KiB
PHP
162 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Social\Telegram;
|
|
|
|
use App\DataTransferObjects\MediaItem;
|
|
use App\Exceptions\Social\TelegramPublishException;
|
|
use App\Models\PostPlatform;
|
|
use App\Models\SocialAccount;
|
|
use App\Services\Social\Concerns\HasSocialHttpClient;
|
|
use App\Services\Social\ContentSanitizer;
|
|
use Illuminate\Http\Client\Response;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class TelegramPublisher
|
|
{
|
|
use HasSocialHttpClient;
|
|
|
|
/**
|
|
* Telegram caps media captions at 1024 chars (a text-only message allows
|
|
* 4096). When the post is longer than a caption, the media is sent first and
|
|
* the full text follows as its own message.
|
|
*/
|
|
private const CAPTION_LIMIT = 1024;
|
|
|
|
private const ALBUM_CHUNK = 10;
|
|
|
|
public function publish(PostPlatform $postPlatform): array
|
|
{
|
|
$this->validateContentLength($postPlatform);
|
|
|
|
$account = $postPlatform->socialAccount;
|
|
$chatId = (string) data_get($account->meta, 'chat_id');
|
|
|
|
$content = $postPlatform->post->content
|
|
? app(ContentSanitizer::class)->sanitize($postPlatform->post->content, $postPlatform->platform)
|
|
: '';
|
|
|
|
$media = $postPlatform->post->mediaItems->take(self::ALBUM_CHUNK);
|
|
|
|
$messageId = $media->isEmpty()
|
|
? $this->sendText($chatId, $content)
|
|
: $this->sendWithMedia($chatId, $content, $media);
|
|
|
|
return [
|
|
'id' => (string) $messageId,
|
|
'url' => $this->buildPostUrl($account, $messageId),
|
|
];
|
|
}
|
|
|
|
private function sendText(string $chatId, string $text): int
|
|
{
|
|
$response = $this->call('sendMessage', [
|
|
'chat_id' => $chatId,
|
|
'text' => $text,
|
|
'parse_mode' => 'HTML',
|
|
]);
|
|
|
|
return (int) data_get($response->json(), 'result.message_id');
|
|
}
|
|
|
|
private function sendWithMedia(string $chatId, string $content, Collection $media): int
|
|
{
|
|
$fitsCaption = mb_strlen($content) <= self::CAPTION_LIMIT;
|
|
$caption = $fitsCaption ? $content : '';
|
|
|
|
$items = $media->map(fn (MediaItem $item) => $this->telegramMedia($item))->values()->all();
|
|
|
|
$messageId = count($items) === 1
|
|
? $this->sendSingleMedia($chatId, $items[0], $caption)
|
|
: $this->sendMediaGroup($chatId, $items, $caption);
|
|
|
|
// Long text can't ride along as a caption — send it as a follow-up message.
|
|
if (! $fitsCaption) {
|
|
$this->sendText($chatId, $content);
|
|
}
|
|
|
|
return $messageId;
|
|
}
|
|
|
|
/**
|
|
* @param array{type: TelegramMediaType, url: string} $item
|
|
*/
|
|
private function sendSingleMedia(string $chatId, array $item, string $caption): int
|
|
{
|
|
$type = $item['type'];
|
|
|
|
$response = $this->call($type->sendMethod(), [
|
|
'chat_id' => $chatId,
|
|
$type->value => $item['url'],
|
|
'caption' => $caption,
|
|
'parse_mode' => 'HTML',
|
|
]);
|
|
|
|
return (int) data_get($response->json(), 'result.message_id');
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{type: TelegramMediaType, url: string}> $items
|
|
*/
|
|
private function sendMediaGroup(string $chatId, array $items, string $caption): int
|
|
{
|
|
$group = [];
|
|
|
|
foreach ($items as $index => $item) {
|
|
$entry = ['type' => $item['type']->value, 'media' => $item['url']];
|
|
|
|
if ($index === 0 && $caption !== '') {
|
|
$entry['caption'] = $caption;
|
|
$entry['parse_mode'] = 'HTML';
|
|
}
|
|
|
|
$group[] = $entry;
|
|
}
|
|
|
|
$response = $this->call('sendMediaGroup', [
|
|
'chat_id' => $chatId,
|
|
'media' => json_encode($group),
|
|
]);
|
|
|
|
return (int) data_get($response->json(), 'result.0.message_id');
|
|
}
|
|
|
|
/**
|
|
* @return array{type: TelegramMediaType, url: string}
|
|
*/
|
|
private function telegramMedia(MediaItem $media): array
|
|
{
|
|
return ['type' => TelegramMediaType::for($media), 'url' => $media->url];
|
|
}
|
|
|
|
private function call(string $method, array $payload): Response
|
|
{
|
|
$response = $this->socialHttp()->post(TelegramApi::endpoint($method), $payload);
|
|
|
|
if ($response->failed() || data_get($response->json(), 'ok') !== true) {
|
|
$this->handleApiError($response);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function buildPostUrl(SocialAccount $account, int $messageId): string
|
|
{
|
|
$username = (string) data_get($account->meta, 'username');
|
|
|
|
if ($username !== '') {
|
|
return "https://t.me/{$username}/{$messageId}";
|
|
}
|
|
|
|
// Private channels: t.me/c/<id without the -100 prefix>/<message_id>.
|
|
$internalId = preg_replace('/^-100/', '', (string) data_get($account->meta, 'chat_id'));
|
|
|
|
return "https://t.me/c/{$internalId}/{$messageId}";
|
|
}
|
|
|
|
private function handleApiError(Response $response): never
|
|
{
|
|
throw TelegramPublishException::fromApiResponse($response);
|
|
}
|
|
}
|