/* global React, ReactDOM */
(() => {
const { useState, useEffect, useRef } = React;
const DEMO_CHANNELS = {
tg: [
{ id: "city-detail", title: "Город в деталях", username: "city_in_detail" },
{ id: "slow-home", title: "Дом без спешки", username: "slow_home" },
],
max: [
{ id: "city-detail-max", title: "Город в деталях | MAX" },
{ id: "slow-home-max", title: "Дом без спешки | MAX" },
],
};
const api = window.POSTILO_DEMO_MODE ? async (path, options = {}) => {
const method = String(options.method || "GET").toUpperCase();
if (method !== "GET") throw new Error("В демо-режиме изменения не сохраняются.");
if (path.startsWith("/api/tg_channels")) return DEMO_CHANNELS.tg;
if (path.startsWith("/api/max_channels")) return DEMO_CHANNELS.max;
if (path === "/api/mappings") return [{ id: "demo-map", mapping_kind: "tg_to_max", tg_channel_id: "city-detail", max_chat_id: "city-detail-max", source_title: "Город в деталях", target_title: "Город в деталях | MAX" }];
if (path.startsWith("/api/mappings/")) return { id: "demo-map", is_active: true, repost_delay_seconds: 60, mapping_signature_text: "" };
return {};
} : window.postiloRequest;
const post = (path, body) => api(path, { method: "POST", body: JSON.stringify(body) });
const errorText = error => error?.message || "Не удалось выполнить действие.";
function Notice({ error, children }) {
return children ?
{children}
: null;
}
function Field({ label, labelAction, trailing, ...props }) {
const id = React.useId();
return
{labelAction}
{trailing}
;
}
function Action({ busy, children, ...props }) {
return ;
}
function DelaySelector({ value, onChange, disabled = false }) {
const options = [{ value: 0, label: "Сразу" }, { value: 60, label: "1 минута" }, { value: 300, label: "5 минут" }, { value: 900, label: "15 минут" }];
return ;
}
function AccountError({ state }) {
return state.error ? {state.error} : null;
}
function ConnectionForm({ platform, onConnected }) {
const isMax = platform === "max";
const name = isMax ? "MAX" : "Telegram";
const [phone, setPhone] = useState("");
const [code, setCode] = useState("");
const [password, setPassword] = useState("");
const [step, setStep] = useState("phone");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [message, setMessage] = useState("");
const [resendAt, setResendAt] = useState(0);
const [expiresAt, setExpiresAt] = useState(0);
const [now, setNow] = useState(Date.now());
const mounted = useRef(true);
useEffect(() => { mounted.current = true; const timer = setInterval(() => setNow(Date.now()), 1000); return () => { mounted.current = false; clearInterval(timer); }; }, []);
const normalized = phone.replace(/[\s()\-]/g, "");
const expired = expiresAt > 0 && now >= expiresAt;
const send = async (resend = false) => {
if (busy) return;
if (!/^\+[1-9]\d{9,14}$/.test(normalized)) { setError("Введите номер с кодом страны, например +79991234567."); return; }
setBusy(true); setError(""); setMessage("");
try {
const data = await post(isMax ? (resend ? "/api/auth/max_otp_resend" : "/api/auth/max_phone_otp") : "/api/auth/tg_mtproto_phone", { phone: normalized });
if (!["code_sent", "code_resent"].includes(data.status)) throw new Error("Сервис не подтвердил отправку кода.");
if (!mounted.current) return;
setPhone(data.phone || normalized); setStep("code"); setCode(""); setPassword("");
setNow(Date.now()); setResendAt(Date.now() + 60000); setExpiresAt(Date.now() + (data.ttl_seconds || 600) * 1000);
setMessage(isMax ? "Запрос кода принят MAX. Проверьте SMS и приложение MAX." : "Код отправлен в Telegram. Откройте приложение и найдите сообщение о входе.");
} catch (e) { if (mounted.current) { setError(errorText(e)); if ([404,410].includes(e.status)) setStep("phone"); } }
finally { if (mounted.current) setBusy(false); }
};
const confirm = async e => {
e.preventDefault();
if (busy) return;
if (step === "phone") { await send(); return; }
if (expired) { setError("Срок действия кода истёк. Запросите новый код."); return; }
setBusy(true); setError(""); setMessage("");
try {
const data = await post(isMax ? "/api/auth/max_otp_confirm" : "/api/auth/tg_mtproto_confirm", { code, ...(step === "password" ? { password } : {}) });
if (!mounted.current) return;
if (data.status === "password_required") { setStep("password"); setMessage("В Telegram включён облачный пароль. Введите его ниже."); return; }
if (!["connected", "logged_in"].includes(data.status)) throw new Error("Подключение ещё не подтверждено. Проверьте статус.");
await window.postiloAccountChanged();
if (mounted.current) { setCode(""); setPassword(""); setStep("done"); setMessage(name + " подключён к вашему аккаунту Postilo."); onConnected?.(); }
} catch (e) { if (mounted.current) { setError(errorText(e)); if (e.status === 410) setStep("phone"); } }
finally { if (mounted.current) setBusy(false); }
};
return ;
}
function BotForm({ onConnected, compact = false }) {
const state = window.usePostiloAccount();
const [token, setToken] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [saved, setSaved] = useState("");
async function submit(e) {
e.preventDefault(); if (busy) return;
if (!/^\d{6,12}:[A-Za-z0-9_-]{30,}$/.test(token.trim())) { setError("Проверьте токен: скопируйте его целиком из BotFather."); return; }
setBusy(true); setError("");
try {
const data = await post("/api/tg_bot_token/verify", { token: token.trim() });
if (!data.ok) throw new Error("Telegram не подтвердил бота.");
setToken(""); setSaved("Подключён бот @" + data.bot_username);
await window.postiloAccountChanged(); onConnected?.();
} catch (e) { setError(errorText(e)); } finally { setBusy(false); }
}
return
{state.user?.tg_bot_connected && !compact && Ваш Telegram-бот: {state.user.tg_bot_username ? @{state.user.tg_bot_username} : "подключён"}. Откройте его в Telegram и нажмите «Запустить», чтобы он мог отправлять вам уведомления. Для другого бота можно заменить токен ниже.}
{compact ? (
{state.user?.tg_bot_connected
? <>Вставьте новый токен, только если хотите заменить подключённого бота. Новый бот должен быть создан через @BotFather.>
: <>Создайте бота через @BotFather, скопируйте токен и вставьте его ниже.>}
) : (
Откройте @BotFather в Telegram и отправьте /newbot.
Задайте название и username бота с окончанием bot. Скопируйте выданный токен.
Вставьте токен ниже. Postilo проверит бота и привяжет его только к вашему аккаунту.
Добавьте именно этого бота администратором нужных Telegram-каналов. Для канала-получателя разрешите публикацию сообщений; для переноса изменений — их редактирование.
)}
Токен хранится зашифрованным. Не подключайте этого же бота одновременно к другому сервису.
Первая доставка подтвержденаPostilo перенёс публикацию из «{result.source_title || mapping.sourceTitle}» в «{result.target_title || mapping.targetTitle}».
Для Telegram → MAX подключите своего Telegram-бота и личный MAX-аккаунт, затем выберите каналы. MAX-бот, ИП или организация не нужны. Telegram-аккаунт дополнительно открывает перенос архива и больших видео.
>}
{mappingError}
{state.loading ?
Загружаем ваши подключения…
: !user ? null : <>
s[0] === section)[1]}>
{steps.find(s => s[0] === section)[1]}
{section === "bot" && }
{section === "tg" && (user.tg_account_connected ? Telegram подключён{user.tg_account_username ? ": @" + user.tg_account_username : ""}. Заменить аккаунт можно в настройках. : )}
{section === "max" && (user.max_connected ? MAX подключён: {user.max_phone}. Заменить аккаунт можно в настройках. : )}
{section === "mapping" && (!mappings?.length
?
: hasFirstDelivery
? Первая реальная доставка подтверждена. Postilo готов переносить новые публикации автоматически.
: <>Связка сохранена, но настройка завершится только после первой реальной доставки.>)}
{mappings?.length > 0 && Сохранённых связок: {mappings.length}. Результат переноса смотрите в журнале репостов. Наличие связки не заменяет проверку первой реальной публикации.}
>}