Corrigiu autenticação do agent

X-Lovable-Edit-ID: edt-f0fa05d9-1883-40bd-898f-f6ec1ef7872e
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot] 2026-04-27 15:50:37 +00:00
commit 26dab520ff
4 changed files with 656 additions and 14 deletions

View file

@ -1,14 +1,14 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
ArrowRight, ArrowRight,
Check, Check,
Copy, Copy,
ExternalLink,
Loader2, Loader2,
AlertCircle, AlertCircle,
MessageCircle,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@ -30,16 +30,24 @@ interface Props {
onSkip: () => void; onSkip: () => void;
} }
type Phase = "configure" | "awaiting_start" | "captured";
export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Props) { export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Props) {
const [phase, setPhase] = useState<Phase>("configure");
const [step1Done, setStep1Done] = useState(false); const [step1Done, setStep1Done] = useState(false);
const [step2Done, setStep2Done] = useState(false); const [step2Done, setStep2Done] = useState(false);
const [step3Done, setStep3Done] = useState(false); const [step3Done, setStep3Done] = useState(false);
const [token, setToken] = useState(""); const [token, setToken] = useState("");
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null); const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [validatedBot, setValidatedBot] = useState<{
bot_username: string;
bot_name: string;
bot_id: number;
} | null>(null);
const pollRef = useRef<number | null>(null);
const suggestedUsername = useMemo(() => { const suggestedUsername = useMemo(() => {
// base do agent_name; cai pro firstName se não der
const base = sanitizeForUsername(agentName).replace(/^mikade/, "mika"); const base = sanitizeForUsername(agentName).replace(/^mikade/, "mika");
if (base.length >= 5) { if (base.length >= 5) {
const trimmed = base.slice(0, 28); const trimmed = base.slice(0, 28);
@ -55,16 +63,6 @@ export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Pr
setStep1Done(true); setStep1Done(true);
} }
async function copyToClipboard(value: string, onDone: () => void) {
try {
await navigator.clipboard.writeText(value);
toast.success("Copiado!");
onDone();
} catch {
toast.error("Não foi possível copiar.");
}
}
async function handleActivate() { async function handleActivate() {
if (!tokenValid) return; if (!tokenValid) return;
setSubmitting(true); setSubmitting(true);
@ -82,7 +80,62 @@ export function BotFatherWizard({ agentName, fullName, onActivated, onSkip }: Pr
); );
return; return;
} }
onActivated(data); setSubmitting(false);
setValidatedBot(data);
setPhase("awaiting_start");
}
function handleOpenMyBot() {
if (!validatedBot?.bot_username) return;
window.open(
`https://t.me/${validatedBot.bot_username}`,
"_blank",
"noopener,noreferrer",
);
}
// Polling: enquanto phase === awaiting_start, chama capture-telegram-owner a cada 2.5s
useEffect(() => {
if (phase !== "awaiting_start") return;
let cancelled = false;
async function tick() {
if (cancelled) return;
const { data, error } = await invokeFunction<{
found: boolean;
chat_id?: number;
first_name?: string;
bot_username?: string;
}>("capture-telegram-owner", {});
if (cancelled) return;
if (error) {
console.warn("capture-telegram-owner error", error);
return;
}
if (data?.found && validatedBot) {
setPhase("captured");
setTimeout(() => onActivated(validatedBot), 1400);
}
}
tick();
pollRef.current = window.setInterval(tick, 2500);
return () => {
cancelled = true;
if (pollRef.current) window.clearInterval(pollRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phase, validatedBot]);
if (phase === "awaiting_start" || phase === "captured") {
return (
<AwaitingStartPanel
botUsername={validatedBot?.bot_username ?? ""}
botName={validatedBot?.bot_name ?? agentName}
captured={phase === "captured"}
onOpenBot={handleOpenMyBot}
/>
);
} }
return ( return (
@ -447,3 +500,95 @@ function ChatPreview({
</div> </div>
); );
} }
function AwaitingStartPanel({
botUsername,
botName,
captured,
onOpenBot,
}: {
botUsername: string;
botName: string;
captured: boolean;
onOpenBot: () => void;
}) {
return (
<div className="mx-auto max-w-xl">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-2xl border border-white/10 bg-white/5 p-6 sm:p-8 text-center"
>
<AnimatePresence mode="wait">
{captured ? (
<motion.div
key="captured"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0 }}
className="space-y-4"
>
<div className="mx-auto h-16 w-16 rounded-full bg-emerald-500/20 flex items-center justify-center">
<Check className="h-8 w-8 text-emerald-400" />
</div>
<h2 className="text-2xl font-bold text-white">
Conectado! 🎉
</h2>
<p className="text-sm text-white/70">
Identificamos você no Telegram. Estamos finalizando a ativação
do seu agente em alguns instantes ele começa a responder.
</p>
</motion.div>
) : (
<motion.div
key="awaiting"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="space-y-5"
>
<div className="mx-auto h-16 w-16 rounded-full bg-primary/20 flex items-center justify-center">
<MessageCircle className="h-8 w-8 text-primary" />
</div>
<div>
<h2 className="text-2xl font-bold text-white">
Última etapa: diga "oi" pro {botName}
</h2>
<p className="mt-2 text-sm text-white/70">
Abra seu bot no Telegram e envie qualquer mensagem (pode
ser <span className="font-mono text-white">/start</span>).
Assim a gente sabe que é você e libera o acesso exclusivo.
</p>
</div>
<Button
size="lg"
onClick={onOpenBot}
className="w-full sm:w-auto sm:min-w-64"
>
Abrir @{botUsername}
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
<div className="flex items-center justify-center gap-2 text-xs text-white/50">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Aguardando sua primeira mensagem
</div>
<div className="rounded-lg border border-white/10 bg-black/20 p-3 text-left">
<p className="text-[11px] uppercase tracking-wide text-white/40">
Por que isso?
</p>
<p className="mt-1 text-xs text-white/70">
Seu agente responde pra você. Ao enviar a primeira
mensagem, capturamos seu ID do Telegram e bloqueamos o bot
para qualquer outra pessoa segurança total.
</p>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
</div>
);
}

View file

@ -0,0 +1,241 @@
// admin-recover-telegram-owner
// Operação ADMIN one-shot para corrigir agentes provisionados ANTES do
// fluxo de captura automática. Faz:
// 1. Suspende o container Hermes (libera getUpdates do bot)
// 2. Long-polling getUpdates por até 90s buscando chat privado humano
// 3. Persiste telegram_user_chat_id em agent_instances
// 4. Atualiza Railway TELEGRAM_ALLOWED_USERS / TELEGRAM_HOME_CHANNEL
// 5. Retoma o container (resume) → o redeploy aplica as novas env vars
//
// O admin chama esta função e simultaneamente pede ao cliente para enviar
// /start no Telegram ao bot dele. A primeira mensagem privada captura o id.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { corsHeaders } from "../_shared/cors.ts";
import { telegramApi } from "../_shared/telegram.ts";
import {
deployRailwayService,
getServiceContext,
setHermesSuspended,
upsertRailwayVariableCollection,
} from "../_shared/railway.ts";
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!;
const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN");
interface TelegramUpdate {
update_id: number;
message?: {
chat?: { id: number; type?: string };
from?: { id: number; is_bot?: boolean; username?: string; first_name?: string };
};
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });
if (!RAILWAY_API_TOKEN) {
return jsonResponse({ error: "RAILWAY_API_TOKEN não configurado" }, 500);
}
// Auth admin
const authHeader = req.headers.get("Authorization") ?? "";
const jwt = authHeader.replace(/^Bearer\s+/i, "");
if (!jwt) return jsonResponse({ error: "missing authorization" }, 401);
const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: { headers: { Authorization: `Bearer ${jwt}` } },
});
const { data: userData, error: userErr } = await userClient.auth.getUser();
if (userErr || !userData?.user) {
return jsonResponse({ error: "invalid token" }, 401);
}
const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
const { data: isAdmin } = await admin.rpc("has_role", {
_user_id: userData.user.id,
_role: "admin",
});
if (!isAdmin) return jsonResponse({ error: "admin role required" }, 403);
const body = await req.json().catch(() => ({}));
const agentId = (body?.agent_instance_id ?? "").toString();
if (!agentId) return jsonResponse({ error: "agent_instance_id required" }, 400);
// Carrega agent
const { data: agent } = await admin
.from("agent_instances")
.select(
"id, status, telegram_bot_token_vault_id, telegram_bot_username, telegram_user_chat_id, railway_service_id, vps_pool_id",
)
.eq("id", agentId)
.maybeSingle();
if (!agent) return jsonResponse({ error: "agent não encontrado" }, 404);
if (!agent.railway_service_id) {
return jsonResponse({ error: "agente sem railway_service_id" }, 409);
}
if (!agent.telegram_bot_token_vault_id) {
return jsonResponse({ error: "agente sem telegram bot token" }, 409);
}
// Resolve project/environment
let projectId: string | null = null;
let environmentId: string | null = null;
if (agent.vps_pool_id) {
const { data: pool } = await admin
.from("vps_pool")
.select("railway_project_id, railway_environment_id")
.eq("id", agent.vps_pool_id)
.maybeSingle();
projectId = pool?.railway_project_id ?? null;
environmentId = pool?.railway_environment_id ?? null;
}
if (!projectId || !environmentId) {
const ctx = await getServiceContext({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
});
projectId = projectId ?? ctx.projectId;
environmentId = environmentId ?? ctx.environmentId;
}
if (!projectId || !environmentId) {
return jsonResponse({ error: "não foi possível resolver railway project/env" }, 500);
}
// Decifra token
const { data: secret } = await admin.rpc("vault_decrypt_secret", {
secret_id: agent.telegram_bot_token_vault_id,
});
// deno-lint-ignore no-explicit-any
const token: string = (secret?.[0] as any)?.decrypted_secret ?? "";
if (!token) return jsonResponse({ error: "falha ao decifrar token" }, 500);
// 1) Suspende Hermes (libera getUpdates) — best effort
try {
await setHermesSuspended({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
projectId,
suspend: true,
});
console.log(`[recover] Hermes suspenso, aguardando 25s para descer`);
} catch (e) {
console.warn("[recover] suspendHermes falhou:", e);
}
// Espera o redeploy de suspend efetivar (Hermes para de consumir updates)
await new Promise((r) => setTimeout(r, 25_000));
// Deleta webhook e dropa pending para garantir polling fresco
await telegramApi(token, "deleteWebhook", { drop_pending_updates: false });
// 2) Long-poll getUpdates — total até ~75s
let ownerChatId: number | null = null;
let ownerFirstName: string | null = null;
let lastOffset = 0;
const deadline = Date.now() + 75_000;
while (Date.now() < deadline && !ownerChatId) {
const remainingSec = Math.max(2, Math.floor((deadline - Date.now()) / 1000));
const timeout = Math.min(20, remainingSec);
const upd = await telegramApi<TelegramUpdate[]>(token, "getUpdates", {
timeout,
offset: lastOffset,
allowed_updates: ["message"],
});
if (!upd.ok) {
console.warn("[recover] getUpdates falhou:", upd);
await new Promise((r) => setTimeout(r, 1500));
continue;
}
const updates = upd.result ?? [];
for (const u of updates) {
if (u.update_id >= lastOffset) lastOffset = u.update_id + 1;
const m = u.message;
if (!m?.chat || !m.from || m.from.is_bot) continue;
if (m.chat.type && m.chat.type !== "private") continue;
ownerChatId = m.chat.id;
ownerFirstName = m.from.first_name ?? null;
break;
}
}
if (!ownerChatId) {
// Retoma Hermes mesmo sem capturar
try {
await setHermesSuspended({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
projectId,
suspend: false,
});
} catch { /* ignore */ }
return jsonResponse({
found: false,
hint: "Cliente não enviou mensagem na janela de 75s. Tente novamente.",
}, 408);
}
// 3) Persiste chat_id no DB
await admin
.from("agent_instances")
.update({
telegram_user_chat_id: ownerChatId,
telegram_first_message_received_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.eq("id", agent.id);
// 4) Mensagem de confirmação ao cliente
await telegramApi(token, "sendMessage", {
chat_id: ownerChatId,
text:
`✅ Tudo certo${ownerFirstName ? `, ${ownerFirstName}` : ""}! Estou finalizando minha ativação. Em alguns instantes começo a conversar com você de verdade. ✨`,
});
// 5) Atualiza Railway env vars TELEGRAM_ALLOWED_USERS / HOME_CHANNEL + retira suspend
const chatIdStr = String(ownerChatId);
await upsertRailwayVariableCollection({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
projectId,
variables: {
TELEGRAM_ALLOWED_USERS: chatIdStr,
TELEGRAM_HOME_CHANNEL: chatIdStr,
HERMES_SUSPENDED: "",
},
skipDeploys: true,
});
await deployRailwayService({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
});
// Volta status para active se estava suspended
await admin
.from("agent_instances")
.update({ status: "active", updated_at: new Date().toISOString() })
.eq("id", agent.id)
.in("status", ["suspended", "active", "provisioning"]);
return jsonResponse({
found: true,
chat_id: ownerChatId,
first_name: ownerFirstName,
redeployed: true,
});
});

View file

@ -0,0 +1,247 @@
// capture-telegram-owner
// Após o cliente conectar o bot (validate-telegram-bot), pedimos a ele que
// envie /start ao próprio bot. Esta função faz polling no getUpdates do
// bot do cliente para descobrir o chat_id do dono. Quando encontra:
// 1) grava telegram_user_chat_id em agent_instances
// 2) envia mensagem de confirmação ao usuário
// 3) limpa o offset (markAsRead) chamando getUpdates com offset alto
// 4) se o agente já está provisionado no Railway, atualiza as env vars
// TELEGRAM_ALLOWED_USERS / TELEGRAM_HOME_CHANNEL e dispara redeploy
//
// É chamada repetidamente pelo frontend (poll a cada 2s) até retornar
// { found: true } ou o usuário desistir.
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
import { corsHeaders } from "../_shared/cors.ts";
import { telegramApi } from "../_shared/telegram.ts";
import {
deployRailwayService,
getServiceContext,
upsertRailwayVariableCollection,
} from "../_shared/railway.ts";
const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN");
interface TelegramUpdate {
update_id: number;
message?: {
chat?: { id: number; type?: string };
from?: { id: number; is_bot?: boolean; username?: string; first_name?: string };
text?: string;
};
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
Deno.serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const authHeader = req.headers.get("Authorization") ?? "";
const userClient = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } },
});
const { data: userData, error: userErr } = await userClient.auth.getUser();
if (userErr || !userData.user) {
return jsonResponse({ error: "Não autenticado" }, 401);
}
const userId = userData.user.id;
const admin = createClient(supabaseUrl, serviceKey);
// 1) Carrega agent + token do Vault
const { data: agent, error: agentErr } = await admin
.from("agent_instances")
.select(
"id, status, telegram_bot_token_vault_id, telegram_bot_username, telegram_user_chat_id, railway_service_id, vps_pool_id",
)
.eq("user_id", userId)
.maybeSingle();
if (agentErr || !agent) {
return jsonResponse({ error: "Agente não encontrado." }, 404);
}
// Já capturado anteriormente — short-circuit
if (agent.telegram_user_chat_id) {
return jsonResponse({
found: true,
chat_id: Number(agent.telegram_user_chat_id),
bot_username: agent.telegram_bot_username,
already_captured: true,
});
}
if (!agent.telegram_bot_token_vault_id) {
return jsonResponse(
{ error: "Bot ainda não conectado. Volte e cole o token do BotFather." },
409,
);
}
const { data: secret } = await admin.rpc("vault_decrypt_secret", {
secret_id: agent.telegram_bot_token_vault_id,
});
// deno-lint-ignore no-explicit-any
const token: string = (secret?.[0] as any)?.decrypted_secret ?? "";
if (!token) {
return jsonResponse({ error: "Falha ao decifrar token." }, 500);
}
// 2) Garante que o webhook está deletado (senão getUpdates falha)
await telegramApi(token, "deleteWebhook", { drop_pending_updates: false });
// 3) Faz getUpdates com timeout curto (long polling 8s) — pega TODAS as mensagens recentes
const updRes = await telegramApi<TelegramUpdate[]>(token, "getUpdates", {
timeout: 8,
allowed_updates: ["message"],
});
if (!updRes.ok) {
console.error("getUpdates failed", updRes);
return jsonResponse(
{ error: updRes.description || "Falha ao consultar Telegram." },
502,
);
}
const updates = updRes.result ?? [];
// 4) Procura a primeira mensagem privada de um humano
let ownerChatId: number | null = null;
let ownerUsername: string | null = null;
let ownerFirstName: string | null = null;
let highestUpdateId = 0;
for (const u of updates) {
if (u.update_id > highestUpdateId) highestUpdateId = u.update_id;
const msg = u.message;
if (!msg) continue;
const chat = msg.chat;
const from = msg.from;
if (!chat || !from || from.is_bot) continue;
// Só aceita chat privado (chat.id === from.id em DMs)
if (chat.type && chat.type !== "private") continue;
ownerChatId = chat.id;
ownerUsername = from.username ?? null;
ownerFirstName = from.first_name ?? null;
break;
}
// 5) Avança o offset para "consumir" os updates lidos (mesmo se não achou,
// limpa lixo antigo). Usar offset = highest+1.
if (highestUpdateId > 0) {
await telegramApi(token, "getUpdates", {
offset: highestUpdateId + 1,
timeout: 0,
limit: 1,
});
}
if (!ownerChatId) {
return jsonResponse({
found: false,
bot_username: agent.telegram_bot_username,
hint: "Envie /start no Telegram ao seu bot para identificá-lo.",
});
}
// 6) Persiste chat_id
await admin
.from("agent_instances")
.update({
telegram_user_chat_id: ownerChatId,
telegram_first_message_received_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.eq("id", agent.id);
// 7) Mensagem de confirmação imediata (é o último "ping" antes do Hermes assumir)
await telegramApi(token, "sendMessage", {
chat_id: ownerChatId,
text:
`✅ Tudo certo${ownerFirstName ? `, ${ownerFirstName}` : ""}! Seu agente está sendo ativado e em alguns instantes começa a conversar com você por aqui. ✨`,
});
// 8) Se já existe serviço Railway, atualiza TELEGRAM_ALLOWED_USERS / HOME_CHANNEL e redeploy
let redeployed = false;
if (agent.railway_service_id && RAILWAY_API_TOKEN) {
try {
let projectId: string | null = null;
let environmentId: string | null = null;
if (agent.vps_pool_id) {
const { data: pool } = await admin
.from("vps_pool")
.select("railway_project_id, railway_environment_id")
.eq("id", agent.vps_pool_id)
.maybeSingle();
projectId = pool?.railway_project_id ?? null;
environmentId = pool?.railway_environment_id ?? null;
}
if (!projectId || !environmentId) {
const ctx = await getServiceContext({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
});
projectId = projectId ?? ctx.projectId;
environmentId = environmentId ?? ctx.environmentId;
}
if (projectId && environmentId) {
const chatIdStr = String(ownerChatId);
await upsertRailwayVariableCollection({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
projectId,
variables: {
TELEGRAM_ALLOWED_USERS: chatIdStr,
TELEGRAM_HOME_CHANNEL: chatIdStr,
},
skipDeploys: true,
});
await deployRailwayService({
token: RAILWAY_API_TOKEN,
serviceId: agent.railway_service_id,
environmentId,
});
redeployed = true;
console.log(
`[capture-telegram-owner] Railway redeploy disparado para serviço ${agent.railway_service_id}`,
);
}
} catch (e) {
console.error(
"[capture-telegram-owner] falha ao atualizar Railway:",
e instanceof Error ? e.message : String(e),
);
}
}
return jsonResponse({
found: true,
chat_id: ownerChatId,
username: ownerUsername,
first_name: ownerFirstName,
bot_username: agent.telegram_bot_username,
redeployed,
});
} catch (err) {
console.error("capture-telegram-owner fatal", err);
return jsonResponse(
{ error: err instanceof Error ? err.message : "Erro inesperado" },
500,
);
}
});

View file

@ -480,6 +480,15 @@ async function handleUpdateExistingService(
HERMES_TTS_PROVIDER: ttsProvider, HERMES_TTS_PROVIDER: ttsProvider,
}; };
// Re-aplica TELEGRAM_ALLOWED_USERS / HOME_CHANNEL se já capturamos chat_id do dono
// (importante para corrigir agentes que foram provisionados sem chat_id e tinham
// que pedir pairing manual).
if (agent.telegram_user_chat_id) {
const chatIdStr = String(agent.telegram_user_chat_id);
variables.TELEGRAM_ALLOWED_USERS = chatIdStr;
variables.TELEGRAM_HOME_CHANNEL = chatIdStr;
}
try { try {
await upsertRailwayVariableCollection({ await upsertRailwayVariableCollection({
token: RAILWAY_API_TOKEN!, token: RAILWAY_API_TOKEN!,