Автопостинг в VK и Telegram из своей админки: полный разбор с кодом, ошибками и лайфхаками
Не хотите разбираться в коде?
Всю инструкцию ниже можно вообще не читать. Просто скопируйте и отправьте боту:
Прочитай инструкцию по ссылке: https://bbb2.ru/product/avtoposting-vk-telegram Настрой мне свой автопостинг в VK и Telegram как там написано. Я подготовлю токены и данные (VK, Telegram, BotFather) — спрашивай по ходу дела.
Бот откроет ссылку, прочитает всё и начнёт спрашивать по шагам. Код, PHP, Node.js — вообще не нужны. Вы просто отвечаете на вопросы. В конце получите работающий автопостинг.
Что подготовить перед началом
| Что нужно | Где взять / как сделать |
|---|---|
| VK access_token | VK ID wall + photos + groups) |
| VK group_id | Сообщество 239867540) |
| Telegram Bot Token | @BotFather 12345:ABCdef... |
| Telegram Канал | Создайте канал, добавьте бота администратором (обязательно через приложение, не через Web) |
| Настройки BotFather | Allow Groups? = ON, Group Privacy = OFF |
| PHP 7.4+ с curl | Уже есть на любом нормальном хостинге |
| Node.js 18+ | Нужен для воркера Telegram (даже на VPS с PHP) |
| npm-пакет undici | npm install undici (для обхода блокировки). Если у вас уже стоит OpenClaw — undici есть в его node_modules, npm install не нужен |
| Cron | Должен быть доступен (на VPS есть всегда) |
Зачем я сделал свой автопостинг
Я веду сайт bbb2.ru, пишу статьи и хочу чтобы они автоматом уходили в VK и Telegram. Сервисов для этого — десятки: SMMplanner, Amplifer, Telepost. Но все они чего-то хотят: денег, доступов, подписок.
Я сел и написал свой автопостинг. Заняло вечер. Теперь он работает прямо из админки сайта — одна кнопка, пост улетает в обе соцсети. Код ниже — ровно то, что работает у меня. Вы можете взять его целиком, показать боту, и он сделает то же самое для вашего сервера.
Кстати, я всё это делал через чат с ботом — не писал код руками, а просто объяснял что нужно. Ниже под каждым блоком кода я покажу что именно я отправлял боту, чтобы вы могли сделать так же.
Как это работает — на пальцах
У меня PHP-админка на сайте. Я захожу в раздел «Автопостинг», пишу текст, опционально прикрепляю картинку — и жму одну из трёх кнопок:
- ВК — пост уходит мгновенно
- Telegram — пост попадает в очередь и уходит в течение 2 минут
- Везде — понятно :)
Под капотом — класс SocialPoster с двумя публичными методами. VK — прямой HTTP-запрос к API ВКонтакте. Telegram — запись в очередь на диске (/tmp/tg_post_queue/), которую разбирает отдельный Node.js-воркер.
Telegram через очередь а не напрямую? Скоро расскажу — там самое интересное.
Часть 1. VK — удивительно просто
Запрос боту в чат:
Настрой мне автопостинг в VK — ключ доступа, ID группы и всё остальное
Что вам понадобится
| Что | Где взять |
|---|---|
| access_token | VK ID |
| group_id | Сообщество |
wall, photos и groups. По умолчанию они не отмечены — и пост с картинкой молча упадёт. Я на этом споткнулся в первый раз. Текстовый пост — один запрос
Для обычного поста без картинки хватает одного вызова:
Запрос боту в чат:
Напиши мне метод для отправки текстового поста в VK через API с токенами из .env
function postToVkText(string $message): array {
$token = getenv('VK_ACCESS_TOKEN');
$groupId = getenv('VK_GROUP_ID');
$params = http_build_query([
'owner_id' => '-' . $groupId, // минус — это группа
'message' => $message,
'access_token' => $token,
'v' => '5.199',
]);
$url = "https://api.vk.com/method/wall.post?{$params}";
$response = @file_get_contents($url);
if ($response === false) {
return ['success' => false, 'error' => 'VK API не ответил'];
}
$data = json_decode($response, true);
if (isset($data['error'])) {
return ['success' => false, 'error' => $data['error']['error_msg']];
}
return ['success' => true, 'post_id' => $data['response']['post_id']];
} Обратите внимание: owner_id группы передаётся с минусом (например -239867540). Без минуса VK думает что вы пишете пользователю, а не в группу. И file_get_contents может вернуть false при проблемах с сетью — всегда проверяйте.
Пост с картинкой — четыре шага в одной функции
В отличие от Telegram, VK не умеет принимать фото и текст в одном запросе. Приходится идти по цепочке из четырёх шагов. Вот полный метод:
Запрос боту в чат:
Напиши мне метод для поста с картинкой в VK (загрузка фото + сохранение + прикрепление)
function postToVkWithPhoto(string $message, string $imagePath): array {
$token = getenv('VK_ACCESS_TOKEN');
$groupId = getenv('VK_GROUP_ID');
if (!file_exists($imagePath)) {
return ['success' => false, 'error' => 'Файл изображения не найден'];
}
// Шаг 1: получаем адрес для загрузки фото
$uploadServer = @file_get_contents(
"https://api.vk.com/method/photos.getWallUploadServer?" .
http_build_query([
'group_id' => $groupId,
'access_token' => $token,
'v' => '5.199',
])
);
if ($uploadServer === false) {
return ['success' => false, 'error' => 'Не удалось получить upload server'];
}
$uploadData = json_decode($uploadServer, true);
if (isset($uploadData['error'])) {
return ['success' => false, 'error' => $uploadData['error']['error_msg']];
}
$uploadUrl = $uploadData['response']['upload_url'];
// Шаг 2: загружаем фото на этот адрес (через curl с CURLFile)
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $uploadUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => ['photo' => new CURLFile($imagePath)],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$photoResponse = curl_exec($ch);
$curlError = curl_error($ch);
curl_close($ch);
if ($photoResponse === false) {
return ['success' => false, 'error' => "Загрузка фото: {$curlError}"];
}
$photoData = json_decode($photoResponse, true);
if (!isset($photoData['photo'])) {
return ['success' => false, 'error' => 'VK не принял фото'];
}
// Шаг 3: сохраняем фото в альбоме группы
$saveResult = @file_get_contents(
"https://api.vk.com/method/photos.saveWallPhoto?" .
http_build_query([
'group_id' => $groupId,
'photo' => $photoData['photo'],
'server' => $photoData['server'],
'hash' => $photoData['hash'],
'access_token' => $token,
'v' => '5.199',
])
);
if ($saveResult === false) {
return ['success' => false, 'error' => 'Не удалось сохранить фото'];
}
$savedData = json_decode($saveResult, true);
if (isset($savedData['error'])) {
return ['success' => false, 'error' => $savedData['error']['error_msg']];
}
$savedPhoto = $savedData['response'][0];
// Шаг 4: публикуем пост с прикреплённым фото
$attachment = "photo{$savedPhoto['owner_id']}_{$savedPhoto['id']}";
$postResult = @file_get_contents(
"https://api.vk.com/method/wall.post?" .
http_build_query([
'owner_id' => '-' . $groupId,
'message' => $message,
'attachment' => $attachment,
'access_token' => $token,
'v' => '5.199',
])
);
if ($postResult === false) {
return ['success' => false, 'error' => 'Пост не опубликован (сетевая ошибка)'];
}
$postData = json_decode($postResult, true);
if (isset($postData['error'])) {
return ['success' => false, 'error' => $postData['error']['error_msg']];
}
return ['success' => true, 'post_id' => $postData['response']['post_id']];
} Четыре запроса на один пост с картинкой — так устроен VK API. Зато для текстового хватает одного. Каждый шаг проверяет ошибки — без этого пост просто молча не выйдет, а вы не узнаете почему.
Часть 2. Telegram — вот где началось приключение
Запрос боту в чат:
Настрой мне автопостинг в Telegram — бот, канал, очередь и воркер
Что подготовить
| Что | Где взять |
|---|---|
| Bot token | @BotFather в Telegram 12345:ABCdef... |
| Канал | Создайте канал в Telegram (например @my_channel) |
| Бот — админ канала | Канал |
Настройки бота в BotFather
После создания бота зайдите в @BotFather /mybots
ваш бот
Bot Settings и проверьте два параметра:
Allow Groups?— должен быть ON (иначе бот не сможет писать в канал)Group Privacy— должен быть OFF (бот должен видеть сообщения, а не только команды)
Allow Groups? выключен, бот молча игнорирует попытки отправить сообщение в канал. Не падает с ошибкой — просто не доставляет. Проверьте эту настройку до того, как начнёте писать код. Прокси для Telegram: MTProto, SOCKS5 и почему это не одно и то же
Если ваш сервер в России — этот раздел для вас. Если сервер за границей — можете смело пропустить.
В чём проблема
Telegram Bot API находится по адресу api.telegram.org. РКН блокирует этот домен (и его IP-адреса) для российских серверов. Прямой запрос через curl с российского VPS просто зависает — таймаут через 30 секунд.
Три вида прокси — и почему два не подходят
| Тип прокси | Что это | Работает с Bot API? | Почему |
|---|---|---|---|
| SOCKS5 | TCP-туннель, пропускает любой трафик | Некоторые SOCKS5-провайдеры (например px6.net) блокируют IP-адреса Telegram на уровне назначения. Прокси работает для обычных сайтов, но не для Telegram | |
| HTTP/HTTPS | Прокси с методом CONNECT | curl работает «из коробки», если прокси не режет Telegram | |
| MTProto | Бинарный протокол клиента Telegram | MTProto — это протокол приложения Telegram на телефоне. Bot API работает через HTTPS REST. Это два совершенно разных протокола. Физически несовместимы. |
В интернете полно статей «прокси для Telegram», и 99% из них — про MTProto. Ссылки вида
tg://proxy?server=...&port=.... Эти прокси созданы чтобы приложение Telegram на вашем телефоне обходило блокировку. Они не передают HTTPS.Я проверил лично: curl через MTProto-прокси до
api.telegram.org не доходит — таймаут. Вывод: для бота ищите SOCKS5 или HTTP-прокси, который не блокирует Telegram на уровне IP. Либо — читайте дальше, я покажу как обойтись вообще без прокси. Как быстро проверить прокси
Запрос боту в чат:
Проверь, работает ли мой SOCKS5-прокси для Telegram API
# Через SOCKS5
curl --socks5-hostname proxy_ip:port \\
--proxy-user user:pass \\
-I "https://api.telegram.org" \\
--connect-timeout 5 --max-time 10
# Через HTTP
curl --proxy http://proxy_ip:port \\
-I "https://api.telegram.org" \\
--connect-timeout 5 --max-time 10 Если ответ пришёл (хоть 200, хоть 404) — прокси работает. Если тишина и таймаут — прокси блокирует Telegram.
Обход блокировки без прокси: фиксированные IP
Пока я тестировал прокси, бот предложил решение которое не требует внешнего прокси вообще.
Как это работает
РКН блокирует api.telegram.org на уровне DNS (определении IP-адреса сайта) и маршрутизации, но не все IP-адреса Telegram API блокируются одинаково. Я использую трёхслойный подход:
- Только IPv4 — принудительно запрашиваем только IPv4-адреса (IPv6 на российских серверах часто ведёт себя непредсказуемо)
- Резервный IP — если стандартный DNS не помог, идём напрямую на
149.154.167.220(один из серверов Telegram API, который стабильно отвечает с российских IP) - Запоминание — если резервный IP сработал, используем его для всех последующих запросов
Код на Node.js — функция для обхода блокировки
Запрос боту в чат:
Напиши мне Node.js скрипт для обхода блокировки Telegram через undici с резервными IP (fallback на 149.154.167.220)
const { Agent, fetch } = require('undici');
const { lookup } = require('node:dns');
const FALLBACK_IPS = ['149.154.167.220'];
// Только IPv4 — избегаем проблем с IPv6 на российских серверах
function ipv4Lookup(hostname, options, callback) {
return lookup(hostname, { ...(options || {}), family: 4 }, callback);
}
async function telegramFetch(url, init) {
// Попытка 1: свой DNS, только IPv4
try {
const agent = new Agent({
connect: { lookup: ipv4Lookup, autoSelectFamily: false }
});
return await fetch(url, { ...init, dispatcher: agent });
} catch (e) { /* идём дальше */ }
// Попытка 2+: резервные IP напрямую (в обход DNS)
for (const ip of FALLBACK_IPS) {
try {
const agent = new Agent({
connect: {
lookup: (h, opts, cb) => cb(null, [{ address: ip, family: 4 }])
}
});
return await fetch(url, {
...init,
dispatcher: agent,
headers: { ...init?.headers, Host: 'api.telegram.org' }
});
} catch (e) { /* пробуем следующий IP */ }
}
throw new Error('Все попытки подключения к Telegram не удались');
} Этот подход работает прямо сейчас с российского сервера. Никакого внешнего прокси. Зависимость одна — npm-пакет undici.
setGlobalDispatcher() был удалён, а интерфейс Dispatcher изменился — старый код с кастомными диспетчерами молча падает с ошибкой invalid onRequestStart method. Код выше написан для undici 7+ и проверен на undici 8.x из OpenClaw 2026.6.10. Если обновляете undici — сверяйтесь с её changelog, паттерны работы с Agent могут измениться. Почему я сделал очередь для Telegram
Я не вызываю Telegram API напрямую из PHP. Вместо этого SocialPoster::postToTelegram() пишет JSON-файл в папку на диске:
Запрос боту в чат:
Напиши мне PHP-код для записи постов в очередь /tmp/tg_post_queue/ (мгновенно, без ожидания ответа Telegram)
// PHP: запись в очередь — мгновенно (не ждём ответа Telegram)
$queueDir = '/tmp/tg_post_queue';
$filename = "post_" . time() . "_" . bin2hex(random_bytes(2)) . ".json";
file_put_contents("{$queueDir}/{$filename}", json_encode([
'channel' => '@my_channel',
'token' => $botToken,
'message' => $message,
'image' => $imageCopyPath, // или null
'timestamp' => time(),
])); А отдельный Node.js-воркер раз в 2 минуты (через крон) подбирает файлы из очереди и отправляет. Вот полный код воркера — tg_queue_worker.js:
Запрос боту в чат:
Напиши мне полный tg_queue_worker.js: чтение очереди из /tmp/tg_post_queue/, отправка через undici с fallback IP, удаление старых файлов
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { Agent, fetch } = require('undici');
const { lookup } = require('node:dns');
const QUEUE_DIR = '/tmp/tg_post_queue';
const FALLBACK_IPS = ['149.154.167.220'];
// --- Telegram Fetch с обходом блокировки ---
function ipv4Lookup(hostname, options, callback) {
return lookup(hostname, { ...(options || {}), family: 4 }, callback);
}
async function telegramFetch(url, init) {
try {
const agent = new Agent({
connect: { lookup: ipv4Lookup, autoSelectFamily: false }
});
return await fetch(url, { ...init, dispatcher: agent });
} catch (e) {}
for (const ip of FALLBACK_IPS) {
try {
const agent = new Agent({
connect: {
lookup: (h, opts, cb) => cb(null, [{ address: ip, family: 4 }])
}
});
return await fetch(url, {
...init,
dispatcher: agent,
headers: { ...init?.headers, Host: 'api.telegram.org' }
});
} catch (e) {}
}
throw new Error('Не удалось подключиться к Telegram API');
}
// --- Отправка одного поста ---
async function sendMessage(token, channel, message) {
const url = `https://api.telegram.org/bot${token}/sendMessage`;
const body = JSON.stringify({
chat_id: channel,
text: message,
parse_mode: 'HTML',
});
const resp = await telegramFetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
});
const data = await resp.json();
if (!data.ok) {
throw new Error(`Telegram API: ${data.description}`);
}
return data;
}
async function sendPhoto(token, channel, message, imagePath) {
const url = `https://api.telegram.org/bot${token}/sendPhoto`;
const FormData = require('undici').FormData;
const form = new FormData();
form.append('chat_id', channel);
form.append('caption', message);
form.append('parse_mode', 'HTML');
form.append('photo', fs.createReadStream(imagePath));
const resp = await telegramFetch(url, {
method: 'POST',
body: form,
// Не ставим Content-Type — undici сам выставит multipart/form-data с boundary
});
const data = await resp.json();
if (!data.ok) {
throw new Error(`Telegram API (photo): ${data.description}`);
}
return data;
}
// --- Обработка очереди ---
async function processQueue() {
if (!fs.existsSync(QUEUE_DIR)) {
console.log(`[${new Date().toISOString()}] Очередь не найдена: ${QUEUE_DIR}`);
return;
}
const files = fs.readdirSync(QUEUE_DIR)
.filter(f => f.startsWith('post_') && f.endsWith('.json'))
.sort(); // Старые первыми
if (files.length === 0) return;
console.log(`[${new Date().toISOString()}] Постов в очереди: ${files.length}`);
for (const file of files) {
const filePath = path.join(QUEUE_DIR, file);
let payload;
try {
payload = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
} catch (e) {
console.error(`⚠️ ${file}: невалидный JSON, удаляю`);
fs.unlinkSync(filePath);
continue;
}
const { token, channel, message, image, timestamp } = payload;
try {
if (image && fs.existsSync(image)) {
await sendPhoto(token, channel, message, image);
// Удаляем временную копию картинки
fs.unlinkSync(image);
} else {
await sendMessage(token, channel, message);
}
console.log(`✅ ${file}: отправлен (${message.substring(0, 50)}...)`);
// Успех — удаляем файл из очереди
fs.unlinkSync(filePath);
} catch (e) {
console.error(`❌ ${file}: ${e.message}`);
// Если файл старше 1 часа — удаляем как безнадёжный
const ageSeconds = (Date.now() / 1000) - (timestamp || 0);
if (ageSeconds > 3600) {
console.error(`⏰ ${file}: старше 1 часа, удаляю`);
fs.unlinkSync(filePath);
if (image && fs.existsSync(image)) fs.unlinkSync(image);
}
// Иначе оставляем — попробуем снова через 2 минуты
}
}
}
// Запуск
processQueue().catch(e => {
console.error(`Критическая ошибка воркера: ${e.message}`);
process.exit(1);
}); Крон — каждые 2 минуты:
Запрос боту в чат:
Добавь запись в crontab: запуск tg_queue_worker.js каждые 2 минуты
*/2 * * * * node /usr/local/bin/tg_queue_worker.js >> /var/log/tg_queue.log 2>&1
Четыре причины, почему так а не напрямую
- Админка не тормозит. Прямой запрос к Telegram может занять 10-30 секунд. Вы нажали кнопку и пошли дальше — пост уйдёт сам.
- Устойчивость к сбоям. Если Telegram временно недоступен — файл остаётся в очереди. Воркер попробует снова через 2 минуты.
- Механизм с fallback IP. Он реализован на Node.js (через undici). Сделать то же самое на голом PHP с curl сложнее и менее надёжно.
- Готовые логи. Каждый файл в очереди — это запись: что, когда и куда отправили. Не нужно отдельного логирования.
Полный код класса SocialPoster
Запрос боту в чат:
Сделай мне класс SocialPoster для автопостинга в VK и Telegram с токенами из .env
Это итоговый класс, который работает у меня на bbb2.ru. Отправьте боту этот код — он адаптирует его под ваш сервер, подставит токены из .env и проверит что все методы работают.
<?php
class SocialPoster {
private string $vkToken;
private string $vkGroupId;
private string $tgToken;
private string $tgChannel;
private string $queueDir;
public function __construct() {
$this->vkToken = getenv('VK_ACCESS_TOKEN');
$this->vkGroupId = getenv('VK_GROUP_ID');
$this->tgToken = getenv('TELEGRAM_BOT_TOKEN');
$this->tgChannel = getenv('TELEGRAM_CHANNEL');
$this->queueDir = '/tmp/tg_post_queue';
if (!is_dir($this->queueDir)) {
mkdir($this->queueDir, 0700, true);
}
}
/** Публикация в VK — автоматически выбирает текст или фото */
public function postToVK(string $message, ?string $imagePath = null): array {
if ($imagePath && file_exists($imagePath)) {
return $this->postToVkWithPhoto($message, $imagePath);
}
return $this->postToVkText($message);
}
/** Публикация в Telegram (через очередь) */
public function postToTelegram(string $message, ?string $imagePath = null): array {
$payload = [
'channel' => $this->tgChannel,
'token' => $this->tgToken,
'message' => $message,
'image' => null,
'timestamp' => time(),
];
if ($imagePath && file_exists($imagePath)) {
$ext = pathinfo($imagePath, PATHINFO_EXTENSION);
$copyPath = "{$this->queueDir}/img_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
copy($imagePath, $copyPath);
$payload['image'] = $copyPath;
}
$filename = "{$this->queueDir}/post_" . time() . "_" . bin2hex(random_bytes(2)) . ".json";
$written = file_put_contents($filename, json_encode($payload, JSON_UNESCAPED_UNICODE));
return [
'success' => (bool)$written,
'method' => 'queue',
'file' => basename($filename),
];
}
// (методы postToVkText и postToVkWithPhoto — см. выше в статье)
} Страница автопостинга в админке
Минимальный пример страницы /admin/social.php — форма с textarea, полем загрузки картинки и тремя кнопками:
Запрос боту в чат:
Напиши мне admin/social.php — страницу автопостинга с формой (textarea, загрузка картинки, кнопки VK/Telegram/Всё вместе)
<?php
require_once __DIR__ . '/../includes/api/SocialPoster.php';
$poster = new SocialPoster();
$message = '';
$result = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = $_POST['message'] ?? '';
$imagePath = null;
// Если загрузили картинку — сохраняем во временный файл
if (!empty($_FILES['image']['tmp_name'])) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$imagePath = '/tmp/post_img_' . time() . '.' . $ext;
move_uploaded_file($_FILES['image']['tmp_name'], $imagePath);
}
$action = $_POST['action'] ?? '';
try {
if ($action === 'vk') {
$result = $poster->postToVK($message, $imagePath);
} elseif ($action === 'tg') {
$result = $poster->postToTelegram($message, $imagePath);
} elseif ($action === 'both') {
$result = [
'vk' => $poster->postToVK($message, $imagePath),
'tg' => $poster->postToTelegram($message, $imagePath),
];
}
} finally {
// Подчищаем временный файл если был
if ($imagePath && file_exists($imagePath)) {
unlink($imagePath);
}
}
}
?>
<div style="max-width:700px; margin:40px auto;">
<h1>Автопостинг</h1>
<?php if ($result): ?>
<div style="background:#e2efe4; padding:16px 20px; border-radius:10px; margin-bottom:20px;">
<pre><?= htmlspecialchars(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre>
</div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data">
<textarea name="message" rows="6" style="width:100%; padding:12px; font:inherit; border:1px solid #ddd; border-radius:8px;"
placeholder="Текст поста..."><?= htmlspecialchars($message) ?></textarea>
<div style="margin:16px 0;">
<label style="cursor:pointer; display:inline-block; padding:8px 16px; background:#f0f0f0; border-radius:6px;">
📎 Прикрепить картинку
<input type="file" name="image" accept="image/*" style="display:none;">
</label>
</div>
<div style="display:flex; gap:12px;">
<button type="submit" name="action" value="vk" style="padding:12px 24px; background:#0077FF; color:#fff; border:none; border-radius:8px; font-size:16px; cursor:pointer;">
📘 ВК
</button>
<button type="submit" name="action" value="tg" style="padding:12px 24px; background:#2AABEE; color:#fff; border:none; border-radius:8px; font-size:16px; cursor:pointer;">
✈️ Telegram
</button>
<button type="submit" name="action" value="both" style="padding:12px 24px; background:#F76230; color:#fff; border:none; border-radius:8px; font-size:16px; cursor:pointer;">
Везде
</button>
</div>
</form>
</div> Развёртывание: пошагово
Запрос боту в чат:
Разверни автопостинг на моём сервере — скопируй файлы, установи зависимости, пропиши переменные, настрой крон
1. Скопируйте файлы на сервер
includes/api/SocialPoster.php ← класс (код выше) admin/social.php ← админ-страница (код выше) tg_queue_worker.js ← воркер (кладёте в корень проекта, где .env)
2. Установите зависимости Node.js
cd /ваш/проект npm install undici
/usr/lib/node_modules/openclaw/node_modules/. Отдельный npm install не нужен — просто укажите NODE_PATH в кроне (см. шаг 4). 3. Добавьте переменные в .env
VK_ACCESS_TOKEN=ваш_токен_vk VK_GROUP_ID=239867540 TELEGRAM_BOT_TOKEN=12345:ABCdef... TELEGRAM_CHANNEL=@my_channel
4. Настройте крон
crontab -e # добавьте одну из строк (замените пути на свои): # Вариант 1: со своим npm install undici */2 * * * * /usr/bin/node /путь/к/tg_queue_worker.js >> /var/log/tg_queue.log 2>&1 # Вариант 2: undici из OpenClaw (без отдельного npm install) */2 * * * * cd /путь/к/проекту && NODE_PATH=/usr/lib/node_modules/openclaw/node_modules /usr/bin/node tg_queue_worker.js >> /var/log/tg_queue.log 2>&1
5. Проверьте
- Зайдите в
/admin/social.php - Напишите тестовый текст
- Нажмите «ВК» — пост должен появиться мгновенно
- Нажмите «Telegram» — пост должен прийти в течение 2 минут
- Проверьте логи:
tail -f /var/log/tg_queue.log
- Проверьте что
nodeдоступен из крона:which node(обычно/usr/bin/node) - Проверьте что undici доступен:
NODE_PATH=/путь/к/node_modules node -e "require('undici')" - Если undici не найден — установите рядом с воркером:
cd $(dirname воркер) && npm install undici - Проверьте права на запись в
/tmp/tg_post_queue/ - Смотрите логи:
tail -f /var/log/tg_queue.log
Частые проблемы и их решения
| # | Симптом | Причина | Как исправить |
|---|---|---|---|
| 1 | VK-пост с картинкой падает | Токен создан без права photos | Пересоздать токен в VK ID, отметить wall + photos + groups |
| 2 | Не могу добавить бота в админы канала | Telegram Web не показывает настройки администраторов | Использовать Telegram Desktop или мобильное приложение |
| 3 | Бот есть в канале, но посты не доходят | Allow Groups? выключен в BotFather | @BotFather |
| 4 | curl до api.telegram.org — таймаут | Сервер в России, прямое соединение заблокировано | Использовать fallback-механизм с резервными IP (см. раздел выше) |
| 5 | MTProto-прокси не работает для бота | MTProto ≠ HTTPS. Прокси для клиента ≠ прокси для Bot API | Искать SOCKS5 или HTTP-прокси. Либо fallback IP — прокси не нужен |
| 6 | SOCKS5-прокси работает для сайтов, но не для Telegram | Провайдер прокси блокирует IP Telegram API | Проверить другой прокси или использовать fallback IP без прокси |
| 7 | VK API отвечает «Access denied» | owner_id указан без минуса | Для группы: owner_id = -group_id |
Что получилось в итоге
| Компонент | Статус | Как работает |
|---|---|---|
| VK — текст | Прямой вызов wall.post | |
| VK — с фото | 4 шага: uploadServer | |
| Telegram — текст | Очередь на диске | |
| Telegram — с фото | Очередь + копия изображения | |
| Деньги | 0 ₽ | Никаких платных сервисов или прокси |
| Админка | Одна страница: textarea + загрузка файла + 3 кнопки |
Идеи для развития
- История постов — отдельная таблица: какой пост, когда, в какую соцсеть, статус доставки
- Отложенный постинг — поле «опубликовать в» с датой и временем
- Превью — перед отправкой показывать как пост выглядит в каждой соцсети
- Статистика — подтягивать просмотры и лайки из VK и Telegram
- Retry для VK — сейчас пост либо прошёл либо упал; добавить пару повторных попыток
За вечер реально поднять автопостинг в обе соцсети с нуля. Я всё это сделал через чат с ботом — просто описывал что нужно и присылал ссылки на документацию. Самое сложное — не код, а навигация по настройкам: BotFather, права токенов, администрирование каналов. Код занимает ~200 строк на PHP и ~100 строк на Node.js для воркера. Ноль внешних сервисов, ноль рублей в месяц. Если не хотите разбираться — просто скопируйте эту статью и отправьте боту, он всё настроит.