"use client" import {useEffect,useRef,useState,useSyncExternalStore} from "react" import {EyeIcon,EyeOffIcon} from "lucide-react" import type {PutTelegramBody} from "@mmapp/contracts/alerts" import {telegramPending,subscribeTelegramPending,setTelegramPending as checkpoint} from "@/lib/alerts-telegram-checkpoint" import {can} from "@/lib/auth" import {ApiClientError} from "@/shared/api/http-client" import {saveAlertsTelegram,testAlertsTelegram} from "@/shared/api/alerts" import {OpsPanel} from "@/components/ops-panel" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" export type TelegramDraft={token:string;chatId:string;messageThreadId:string;connected:boolean} function thread(value:string){if(!value.trim())return null;if(!/^\d+$/.test(value.trim()))throw Error("ID темы: укажите положительное целое число или оставьте поле пустым");const n=Number(value);if(!Number.isSafeInteger(n)||n<1||n>2147483647)throw Error("ID темы должен быть от 1 до 2147483647");return n} export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,onBegin,onEnd,onSaved}:{initial:TelegramDraft;tokenConfigured:boolean;base:string;live:boolean;available:boolean;onBegin:()=>boolean;onEnd:()=>void;onSaved:(value:TelegramDraft,configured:boolean)=>void}){ const source=JSON.stringify([live,base]) const [draft,setDraft]=useState>>({}) const [showToken,setShowToken]=useState(false),[busy,setBusy]=useState<"save"|"test"|null>(null),[error,setError]=useState(null),[outcome,setOutcome]=useState(null),[ready,setReady]=useState(false) const unknown=useSyncExternalStore(subscribeTelegramPending,()=>telegramPending(source),()=>false) const active=useRef(true),lock=useRef(false),controller=useRef(null) useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source]) const value={...initial,...draft},allowed=can("mm:alerts:write"),enabled=ready&&live&&available&&allowed const dirty=Object.keys(draft).some(key=>value[key as keyof typeof draft]!==initial[key as keyof typeof draft]) function current(){return active.current&&can("mm:alerts:write")} function change(key:keyof typeof draft,value:string){setDraft(previous=>({...previous,[key]:value}));setError(null);setOutcome(null)} async function execute(kind:"save"|"test"){ if(!enabled||lock.current||unknown||telegramPending(source)||!current())return const payload:PutTelegramBody={} try{ const messageThreadId=thread(value.messageThreadId) if(kind==="save"){ if(draft.chatId!==undefined&&value.chatId!==initial.chatId)payload.chatId=value.chatId if(draft.messageThreadId!==undefined&&value.messageThreadId!==initial.messageThreadId)payload.messageThreadId=messageThreadId if(value.token.trim())payload.token=value.token.trim() if(!Object.keys(payload).length){setError("Нет изменённых параметров Telegram");return} }else{ if(!value.chatId.trim())throw Error("Укажите Chat ID для тестового сообщения") if(!value.token.trim()&&!tokenConfigured)throw Error("Укажите токен или сохраните его в БД") if(draft.messageThreadId!==undefined&&!messageThreadId&&initial.messageThreadId)throw Error("Сначала сохраните очистку ID темы: тестовый API использует сохранённую тему, если ID не указан") } }catch(e){setError(e instanceof Error?e.message:"Проверьте параметры Telegram");return} if(!onBegin())return lock.current=true;controller.current=new AbortController();setBusy(kind);setError(null);setOutcome(null) checkpoint(source,true) try{ if(kind==="save"){ const result=await saveAlertsTelegram(base,payload,{signal:controller.current.signal});if(!current())return const saved=result.telegram onSaved({token:"",chatId:saved.chatId,messageThreadId:saved.messageThreadId===null?"":String(saved.messageThreadId),connected:saved.connected},saved.tokenConfigured) setDraft({});setOutcome("Параметры Telegram сохранены в БД") }else{ const messageThreadId=thread(value.messageThreadId) await testAlertsTelegram(base,{chatId:value.chatId.trim(),...(value.token.trim()?{token:value.token.trim()}:{}),...(messageThreadId!==null?{messageThreadId}:{})},{signal:controller.current.signal});if(!current())return setOutcome("Telegram API подтвердил приём тестового сообщения. Проверьте его в чате.") } checkpoint(source,false) }catch(e){ const rejected=e instanceof ApiClientError&&[400,401,403,409,422].includes(e.status) if(rejected&¤t())checkpoint(source,false) if(current()){setError(e instanceof Error?e.message:"Результат Telegram не получен")} }finally{lock.current=false;controller.current=null;if(active.current){setBusy(null);onEnd()}} } return
change("token",e.target.value)} autoComplete="new-password" placeholder={tokenConfigured?"Токен сохранён; введите новый для замены":"Токен Telegram бота"} className="min-w-0 font-mono text-xs"/>
change("chatId",e.target.value)} className="min-w-0 font-mono text-xs"/>

Для групп используйте отрицательный ID. Правило может задать свой чат.

change("messageThreadId",e.target.value)} className="min-w-0 font-mono text-xs"/>

Пустое поле при сохранении очищает тему. Только для форум-супергрупп.

{dirty?

Есть несохранённые изменения. Обновление страницы данных сохранит черновик.

:null} {!live?

Демо: сохранение и отправка недоступны.

:!allowed?

Нет права изменять оповещения.

:null} {error?

{error}

:null} {outcome?

{outcome}

:null} {busy?

Ожидаем подтверждение сервера…

:null} {unknown&&!busy?

Результат запроса неизвестен. Он мог выполниться. Перед повтором проверьте параметры в БД или сообщение в Telegram; обновление данных не подтверждает отправку.

:null}
}