2026-06-14 13:18:44 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Actions\SocialAccount;
|
|
|
|
|
|
|
|
|
|
use App\Enums\SocialAccount\Platform;
|
|
|
|
|
use App\Enums\SocialAccount\Status;
|
2026-06-14 13:58:13 +00:00
|
|
|
use App\Events\TelegramChannelConnected;
|
2026-06-14 13:18:44 +00:00
|
|
|
use App\Features\SocialAccountLimit;
|
|
|
|
|
use App\Models\SocialAccount;
|
|
|
|
|
use App\Models\Workspace;
|
2026-06-14 15:41:19 +00:00
|
|
|
use App\Services\Social\TelegramApi;
|
2026-06-14 13:18:44 +00:00
|
|
|
use Illuminate\Support\Facades\Cache;
|
2026-06-14 13:30:17 +00:00
|
|
|
use Illuminate\Support\Facades\Http;
|
2026-06-14 13:18:44 +00:00
|
|
|
use Laravel\Pennant\Feature;
|
2026-06-14 13:30:17 +00:00
|
|
|
use Throwable;
|
2026-06-14 13:18:44 +00:00
|
|
|
|
|
|
|
|
class ConnectTelegramChannel
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Link a Telegram chat to a workspace for a one-off connect nonce.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $chat The `chat` object from the Bot API update.
|
|
|
|
|
* @return SocialAccount|null The linked account, or null when blocked (account
|
|
|
|
|
* limit reached or the code was already consumed).
|
|
|
|
|
*/
|
|
|
|
|
public static function execute(Workspace $workspace, array $chat, string $nonce): ?SocialAccount
|
|
|
|
|
{
|
|
|
|
|
$chatId = (string) data_get($chat, 'id');
|
|
|
|
|
$username = data_get($chat, 'username');
|
|
|
|
|
|
|
|
|
|
// Block only brand-new accounts against the plan limit, never reconnects.
|
|
|
|
|
$isNewAccount = ! $workspace->socialAccounts()
|
|
|
|
|
->where('platform', Platform::Telegram->value)
|
|
|
|
|
->where('platform_user_id', $chatId)
|
|
|
|
|
->exists();
|
|
|
|
|
|
|
|
|
|
if ($isNewAccount && self::workspaceAtAccountLimit($workspace)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consume the code once so a leaked code can't be replayed to link another chat.
|
|
|
|
|
if (! Cache::add("telegram:connect:{$nonce}", true, now()->addMinutes(15))) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 13:58:13 +00:00
|
|
|
$account = $workspace->socialAccounts()->updateOrCreate(
|
2026-06-14 13:18:44 +00:00
|
|
|
[
|
|
|
|
|
'platform' => Platform::Telegram->value,
|
|
|
|
|
'platform_user_id' => $chatId,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'username' => $username,
|
|
|
|
|
'display_name' => data_get($chat, 'title') ?? $username ?? "Telegram {$chatId}",
|
2026-06-14 13:30:17 +00:00
|
|
|
'avatar_url' => self::fetchChannelAvatar($chatId),
|
2026-06-14 13:18:44 +00:00
|
|
|
'access_token' => '',
|
|
|
|
|
'refresh_token' => '',
|
|
|
|
|
'token_expires_at' => null,
|
|
|
|
|
'scopes' => [],
|
|
|
|
|
'status' => Status::Connected,
|
|
|
|
|
'error_message' => null,
|
|
|
|
|
'disconnected_at' => null,
|
|
|
|
|
'meta' => [
|
|
|
|
|
'chat_id' => $chatId,
|
|
|
|
|
'username' => $username,
|
|
|
|
|
'type' => data_get($chat, 'type'),
|
|
|
|
|
'connect_nonce' => $nonce,
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-06-14 13:58:13 +00:00
|
|
|
|
|
|
|
|
TelegramChannelConnected::dispatch($workspace->id, $nonce);
|
|
|
|
|
|
|
|
|
|
return $account;
|
2026-06-14 13:30:17 +00:00
|
|
|
}
|
2026-06-14 13:18:44 +00:00
|
|
|
|
2026-06-14 13:30:17 +00:00
|
|
|
/**
|
|
|
|
|
* Download the channel's photo via the Bot API and store it, returning the path.
|
|
|
|
|
*/
|
|
|
|
|
private static function fetchChannelAvatar(string $chatId): ?string
|
|
|
|
|
{
|
2026-06-14 15:41:19 +00:00
|
|
|
if (TelegramApi::token() === '') {
|
2026-06-14 13:30:17 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
2026-06-14 13:18:44 +00:00
|
|
|
|
2026-06-14 13:30:17 +00:00
|
|
|
try {
|
2026-06-14 15:41:19 +00:00
|
|
|
$fileId = data_get(Http::get(TelegramApi::endpoint('getChat'), ['chat_id' => $chatId])->json(), 'result.photo.big_file_id');
|
2026-06-14 13:30:17 +00:00
|
|
|
|
|
|
|
|
if (! is_string($fileId)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 15:41:19 +00:00
|
|
|
$filePath = data_get(Http::get(TelegramApi::endpoint('getFile'), ['file_id' => $fileId])->json(), 'result.file_path');
|
2026-06-14 13:30:17 +00:00
|
|
|
|
|
|
|
|
if (! is_string($filePath)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 15:41:19 +00:00
|
|
|
return uploadFromUrl(TelegramApi::fileUrl($filePath));
|
2026-06-14 13:30:17 +00:00
|
|
|
} catch (Throwable) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-06-14 13:18:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function workspaceAtAccountLimit(Workspace $workspace): bool
|
|
|
|
|
{
|
|
|
|
|
if (config('trypost.self_hosted')) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$limit = Feature::for($workspace->account)->value(SocialAccountLimit::class);
|
|
|
|
|
|
|
|
|
|
return $workspace->socialAccounts()->count() >= $limit;
|
|
|
|
|
}
|
|
|
|
|
}
|