diff --git a/app/Actions/SocialAccount/ConnectTelegramChannel.php b/app/Actions/SocialAccount/ConnectTelegramChannel.php index b5711919..45743922 100644 --- a/app/Actions/SocialAccount/ConnectTelegramChannel.php +++ b/app/Actions/SocialAccount/ConnectTelegramChannel.php @@ -6,6 +6,7 @@ use App\Enums\SocialAccount\Platform; use App\Enums\SocialAccount\Status; +use App\Events\TelegramChannelConnected; use App\Features\SocialAccountLimit; use App\Models\SocialAccount; use App\Models\Workspace; @@ -43,7 +44,7 @@ public static function execute(Workspace $workspace, array $chat, string $nonce) return null; } - return $workspace->socialAccounts()->updateOrCreate( + $account = $workspace->socialAccounts()->updateOrCreate( [ 'platform' => Platform::Telegram->value, 'platform_user_id' => $chatId, @@ -67,6 +68,10 @@ public static function execute(Workspace $workspace, array $chat, string $nonce) ], ], ); + + TelegramChannelConnected::dispatch($workspace->id, $nonce); + + return $account; } /** diff --git a/app/Enums/SocialAccount/TelegramConnectStatus.php b/app/Enums/SocialAccount/TelegramConnectStatus.php deleted file mode 100644 index 56c006f9..00000000 --- a/app/Enums/SocialAccount/TelegramConnectStatus.php +++ /dev/null @@ -1,22 +0,0 @@ -workspaceId}"), + ]; + } + + /** + * @return array + */ + public function broadcastWith(): array + { + return [ + 'nonce' => $this->nonce, + ]; + } + + public function broadcastQueue(): string + { + return 'broadcasts'; + } +} diff --git a/app/Http/Controllers/Auth/TelegramController.php b/app/Http/Controllers/Auth/TelegramController.php index bf84f7e3..329bf8a1 100644 --- a/app/Http/Controllers/Auth/TelegramController.php +++ b/app/Http/Controllers/Auth/TelegramController.php @@ -5,7 +5,6 @@ namespace App\Http\Controllers\Auth; use App\Enums\SocialAccount\Platform as SocialPlatform; -use App\Enums\SocialAccount\TelegramConnectStatus; use App\Services\Social\TelegramConnectCode; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -18,7 +17,8 @@ class TelegramController extends SocialController /** * Start a connection: issue a signed one-off code the user posts in their * channel (`/connect `). The code carries the workspace, so the webhook - * can link the channel without any persisted state. + * can link the channel without any persisted state. The returned `nonce` lets + * the UI recognise its own connection on the broadcast channel. */ public function connect(Request $request): JsonResponse { @@ -31,35 +31,13 @@ public function connect(Request $request): JsonResponse $this->ensureSocialAccountLimit($workspace); $expiresAt = now()->addMinutes(15); + $code = TelegramConnectCode::issue($workspace->id, $expiresAt); return response()->json([ - 'code' => TelegramConnectCode::issue($workspace->id, $expiresAt), + 'code' => $code, + 'nonce' => data_get(TelegramConnectCode::decode($code), 'nonce'), 'bot_username' => config('trypost.platforms.telegram.bot_username'), 'expires_at' => $expiresAt->toIso8601String(), ]); } - - /** - * Poll whether the channel for the given code has been linked yet. - */ - public function status(Request $request): JsonResponse - { - $workspace = $request->user()->currentWorkspace; - abort_if($workspace === null, SymfonyResponse::HTTP_CONFLICT, 'No active workspace.'); - - $payload = TelegramConnectCode::decode($request->query('code')); - - if ($payload === null) { - return response()->json(['status' => TelegramConnectStatus::Unknown->value]); - } - - $account = $workspace->socialAccounts() - ->where('platform', SocialPlatform::Telegram->value) - ->where('meta->connect_nonce', data_get($payload, 'nonce')) - ->first(); - - return response()->json([ - 'status' => TelegramConnectStatus::for($account)->value, - ]); - } } diff --git a/resources/js/components/accounts/TelegramConnectDialog.vue b/resources/js/components/accounts/TelegramConnectDialog.vue index 0f875f63..ce84b8d6 100644 --- a/resources/js/components/accounts/TelegramConnectDialog.vue +++ b/resources/js/components/accounts/TelegramConnectDialog.vue @@ -20,15 +20,10 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; +import { useWorkspaceEcho } from '@/composables/echo/useWorkspaceEcho'; +import dayjs from '@/dayjs'; import { copyToClipboard } from '@/lib/utils'; -import { - connect as connectTelegram, - status as telegramStatus, -} from '@/routes/app/social/telegram'; -import { - TelegramConnectStatus, - type TelegramConnectStatusValue, -} from '@/types/telegram-connect-status'; +import { connect as connectTelegram } from '@/routes/app/social/telegram'; const open = defineModel('open', { required: true }); @@ -36,64 +31,47 @@ type Phase = 'loading' | 'ready' | 'connected' | 'expired' | 'error'; interface ConnectResponse { code: string; + nonce: string; bot_username: string; expires_at: string; } -const POLL_INTERVAL_MS = 3000; const SUCCESS_CLOSE_DELAY_MS = 1200; const phase = ref('loading'); const code = ref(''); +const nonce = ref(''); const botUsername = ref(''); const errorMessage = ref(''); const httpConnect = useHttp, ConnectResponse>({}); -const httpStatus = useHttp< - Record, - { status: TelegramConnectStatusValue } ->({}); -let pollTimer: ReturnType | null = null; +let expiryTimer: ReturnType | null = null; -const stopPolling = () => { - if (pollTimer !== null) { - clearTimeout(pollTimer); - pollTimer = null; +const clearExpiry = () => { + if (expiryTimer !== null) { + clearTimeout(expiryTimer); + expiryTimer = null; } }; -const poll = async () => { - if (phase.value !== 'ready') return; - - try { - const response = await httpStatus.get( - telegramStatus.url({ query: { code: code.value } }), - ); - - if (response?.status === TelegramConnectStatus.Connected) { - phase.value = 'connected'; - stopPolling(); - toast.success(trans('accounts.telegram.connected_toast')); - setTimeout(() => { - open.value = false; - router.reload(); - }, SUCCESS_CLOSE_DELAY_MS); +// The channel is linked server-side by the webhook; Reverb pushes the result here. +useWorkspaceEcho<{ nonce: string }>( + '.telegram.channel.connected', + (payload) => { + if (phase.value !== 'ready' || payload.nonce !== nonce.value) { return; } - // The signed code expired (or the session was lost): prompt a fresh one. - if (response?.status === TelegramConnectStatus.Unknown) { - phase.value = 'expired'; - stopPolling(); - return; - } - } catch { - // Transient polling failures are ignored; the next tick retries. - } - - pollTimer = setTimeout(poll, POLL_INTERVAL_MS); -}; + phase.value = 'connected'; + clearExpiry(); + toast.success(trans('accounts.telegram.connected_toast')); + setTimeout(() => { + open.value = false; + router.reload(); + }, SUCCESS_CLOSE_DELAY_MS); + }, +); const start = async () => { phase.value = 'loading'; @@ -102,9 +80,19 @@ const start = async () => { try { const response = await httpConnect.post(connectTelegram.url()); code.value = response.code; + nonce.value = response.nonce; botUsername.value = response.bot_username; phase.value = 'ready'; - poll(); + + clearExpiry(); + expiryTimer = setTimeout( + () => { + if (phase.value === 'ready') { + phase.value = 'expired'; + } + }, + Math.max(0, dayjs(response.expires_at).diff(dayjs())), + ); } catch (error) { phase.value = 'error'; errorMessage.value = @@ -124,11 +112,11 @@ watch(open, (isOpen) => { if (isOpen) { start(); } else { - stopPolling(); + clearExpiry(); } }); -onUnmounted(stopPolling); +onUnmounted(clearExpiry);