سفارش ثبت می‌شود، شما دو ساعت بعد می‌فهمید. هر افزونه‌ی تلگرام ووکامرس هم که نصب می‌کنید، در لاگ وردپرس و یادداشت سفارش همان یک خطا را ثبت می‌کند: cURL error 7: Failed to connect to api.telegram.org port 443: Connection refused — یا نسخه‌ی صبورترش، cURL error 28: Connection timed out. این مقاله سه راه کارکردنی برای رد کردن این خطا را می‌دهد، با کد کامل و لیست چیزهایی که سر راه خراب می‌شود.


چرا این مشکل پیش می‌آید

سه چیز همزمان دست به دست هم می‌دهند:

۱. سرور شما نمی‌تواند به تلگرام وصل شود. وقتی افزونه‌ای می‌خواهد پیام بفرستد، PHP روی سرور شما یک درخواست HTTPS به api.telegram.org می‌زند. api.telegram.org از داخل ایران فیلتر است.

کدام خطا را می‌گیرید، به نحوه‌ی مسدودسازی بستگی دارد و خودش سرنخ عیب‌یابی است:

  • cURL error 7: Connection refused — فایروال بسته‌ی RST تزریق کرده. اتصال فوراً رد می‌شود.
  • cURL error 28: Connection timed out — بسته‌ها بی‌صدا drop می‌شوند. سرور تا سقف timeout منتظر می‌ماند.

خطای دوم بدتر است، چون هر سفارش چند ثانیه سرور را قفل می‌کند. هر دو حالت ربطی به بات، توکن یا chat ID ندارند — درخواست اصلاً از سرور بیرون نمی‌رود.

۲. تحریم از سمت مقابل. حتی اگر فیلترینگ نبود، بخشی از سرویس‌های واسط، IP ایران را خودشان بلاک می‌کنند. برای همین راه‌حل «یک سرویس رایگان خارجی پیدا کن» معمولاً بعد از دو هفته می‌میرد.

۳. وردپرس پروکسی SOCKS را بومی پشتیبانی نمی‌کند. کلاس WP_HTTP_Proxy فقط پروکسی HTTP با احراز هویت BASIC را می‌شناسد. اکثر پروکسی‌هایی که دم دست ایرانی‌هاست SOCKS5 است. یعنی حتی وقتی پروکسی دارید، وردپرس بدون کد اضافه از آن استفاده نمی‌کند.

نتیجه: مشکل شما «افزونه‌ی درست» نیست. مشکل شما مسیر خروج ترافیک است. تا این را حل نکنید، هیچ افزونه‌ای کار نمی‌کند.


راه‌حل، قدم به قدم، با کد

سه روش. از پایدارترین به سریع‌ترین.

روش ۱: رله روی Cloudflare Workers (پیشنهاد اصلی)

منطق ساده است: سرور شما به‌جای تلگرام، به یک آدرس روی Cloudflare می‌زند. Cloudflare پیام را به تلگرام می‌رساند. رایگان است، سرور خارجی نمی‌خواهد، و پلن رایگان روزانه ۱۰۰٬۰۰۰ درخواست می‌دهد که برای هر فروشگاهی بیش از حد کافی است.

قدم ۱ — بات را بسازید. در تلگرام به @BotFather پیام بدهید، /newbot بزنید، توکن را بردارید. بعد بات را به گروه یا کانال مدیران اضافه کنید و ادمینش کنید. برای گرفتن chat ID، در همان گروه به @getmyid_bot پیام بدهید.

قدم ۲ — Worker را بسازید. در داشبورد Cloudflare یک Worker جدید بسازید و این کد را داخلش بگذارید:

const SHARED_SECRET = "CHANGE_ME_TO_A_LONG_RANDOM_STRING";

export default {
  async fetch(request) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    if (request.headers.get("X-Relay-Secret") !== SHARED_SECRET) {
      return new Response("Forbidden", { status: 403 });
    }

    let payload;
    try {
      payload = await request.json();
    } catch (e) {
      return new Response("Bad Request", { status: 400 });
    }

    const { bot_token, method, params } = payload;
    if (!bot_token || !method) {
      return new Response("Missing bot_token or method", { status: 400 });
    }

    const upstream = `https://api.telegram.org/bot${bot_token}/${method}`;

    const tgResponse = await fetch(upstream, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(params || {}),
    });

    return new Response(await tgResponse.text(), {
      status: tgResponse.status,
      headers: { "Content-Type": "application/json" },
    });
  },
};

قدم ۳ — دامنه‌ی شخصی وصل کنید. این مهم‌ترین قدم است و اکثر آموزش‌ها جا می‌اندازندش: دامنه‌ی پیش‌فرض *.workers.dev از داخل ایران فیلتر است. اگر روی آدرس پیش‌فرض بمانید، دقیقاً به همان بن‌بست قبلی برخورد می‌کنید.

یک ساب‌دامین از دامنه‌ی خودتان (مثلاً relay.yoursite.ir) را در Cloudflare به Worker وصل کنید:

Workers & Pages → your-worker → Settings → Domains & Routes → Add Custom Domain

قدم ۴ — از روی سرور تست کنید. SSH بزنید به سرور خودتان، نه لپ‌تاپ:

curl -s -X POST https://relay.yoursite.ir \
  -H "Content-Type: application/json" \
  -H "X-Relay-Secret: CHANGE_ME_TO_A_LONG_RANDOM_STRING" \
  -d '{"bot_token":"123456:ABC-DEF","method":"sendMessage","params":{"chat_id":"-1001234567890","text":"relay ok"}}'

اگر {"ok":true,...} گرفتید، مسیر باز است. اگر Connection refused یا timeout گرفتید، دامنه یا DNS درست ست نشده.

قدم ۵ — کد وردپرس. این را در یک افزونه‌ی کوچک یا در functions.php قالب فرزند بگذارید:

<?php
defined( 'TG_RELAY_URL' )    || define( 'TG_RELAY_URL',    'https://relay.yoursite.ir' );
defined( 'TG_RELAY_SECRET' ) || define( 'TG_RELAY_SECRET', 'CHANGE_ME_TO_A_LONG_RANDOM_STRING' );
defined( 'TG_BOT_TOKEN' )    || define( 'TG_BOT_TOKEN',    '123456:ABC-DEF' );
defined( 'TG_CHAT_ID' )      || define( 'TG_CHAT_ID',      '-1001234567890' );

/**
 * Queue the notification. Never send inside the checkout request.
 * Args are positional on purpose: Action Scheduler spreads them with
 * call_user_func_array, and on PHP 8 string keys become named arguments.
 */
add_action( 'woocommerce_order_status_processing', 'hami_queue_order_notice', 10, 1 );

function hami_queue_order_notice( $order_id ) {
    if ( ! $order_id ) {
        return;
    }
    // 4th arg = $unique: prevents a duplicate pending action for the same order.
    as_enqueue_async_action( 'hami_send_order_notice', array( (int) $order_id ), 'telegram', true );
}

add_action( 'hami_send_order_notice', 'hami_send_order_notice_handler', 10, 1 );

function hami_send_order_notice_handler( $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order ) {
        return;
    }

    // Idempotency guard: never notify twice for the same order.
    if ( $order->get_meta( '_hami_tg_sent' ) ) {
        return;
    }

    $lines   = array();
    $lines[] = '🛒 <b>سفارش جدید</b> #' . $order->get_order_number();
    $lines[] = '👤 ' . esc_html( $order->get_formatted_billing_full_name() );
    $lines[] = '📞 ' . esc_html( $order->get_billing_phone() );
    $lines[] = '💰 ' . wp_strip_all_tags( $order->get_formatted_order_total() );
    $lines[] = '💳 ' . esc_html( $order->get_payment_method_title() );
    $lines[] = '';

    // Trim the item list BEFORE building HTML, never the finished string.
    $items      = $order->get_items();
    $item_count = count( $items );
    $max_items  = 12;

    foreach ( array_slice( $items, 0, $max_items ) as $item ) {
        $lines[] = '• ' . esc_html( $item->get_name() ) . ' × ' . $item->get_quantity();
    }

    if ( $item_count > $max_items ) {
        $lines[] = '<i>و ' . ( $item_count - $max_items ) . ' قلم دیگر…</i>';
    }

    $lines[] = '';
    $lines[] = '<a href="' . esc_url( $order->get_edit_order_url() ) . '">مشاهده در پیشخوان</a>';

    $response = wp_remote_post(
        TG_RELAY_URL,
        array(
            'timeout' => 15,
            'headers' => array(
                'Content-Type'   => 'application/json',
                'X-Relay-Secret' => TG_RELAY_SECRET,
            ),
            'body'    => wp_json_encode(
                array(
                    'bot_token' => TG_BOT_TOKEN,
                    'method'    => 'sendMessage',
                    'params'    => array(
                        'chat_id'              => TG_CHAT_ID,
                        'text'                 => implode( "\n", $lines ),
                        'parse_mode'           => 'HTML',
                        'link_preview_options' => array( 'is_disabled' => true ),
                    ),
                )
            ),
        )
    );

    // Network-level failure: relay unreachable, DNS, TLS.
    if ( is_wp_error( $response ) ) {
        $order->add_order_note( 'Telegram: relay unreachable — ' . $response->get_error_message() );
        hami_schedule_telegram_retry( $order_id );
        return;
    }

    $status = wp_remote_retrieve_response_code( $response );
    $body   = json_decode( wp_remote_retrieve_body( $response ), true );

    // Transient: Telegram rate limit or relay-side error. Worth retrying.
    if ( 429 === $status || $status >= 500 ) {
        $order->add_order_note( "Telegram: transient error {$status}, retrying in 5 min" );
        hami_schedule_telegram_retry( $order_id );
        return;
    }

    // Permanent: bad token, bad chat_id, broken entities. Retrying is pointless.
    if ( empty( $body['ok'] ) ) {
        $order->add_order_note( 'Telegram: permanent API error — ' . wp_remote_retrieve_body( $response ) );
        return;
    }

    $order->update_meta_data( '_hami_tg_sent', current_time( 'mysql' ) );
    $order->save();
}

/**
 * Action Scheduler does NOT retry failed async actions — only recurring ones
 * get rescheduled. Retries have to be scheduled explicitly.
 */
function hami_schedule_telegram_retry( $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order ) {
        return;
    }

    $retries = (int) $order->get_meta( '_hami_tg_retry_count' );
    if ( $retries >= 3 ) {
        $order->add_order_note( 'Telegram: giving up after 3 attempts' );
        $order->save();
        return;
    }

    $order->update_meta_data( '_hami_tg_retry_count', $retries + 1 );
    $order->save();

    as_schedule_single_action( time() + 300, 'hami_send_order_notice', array( (int) $order_id ), 'telegram' );
}

چهار نکته‌ی این کد که فرقش را با اسنیپت‌های معمول می‌سازد:

  • ارسال داخل صف Action Scheduler می‌رود، پس checkout کند نمی‌شود.
  • Retry دستی زمان‌بندی شده. خیلی‌ها فکر می‌کنند اگر داخل callback یک Exception پرتاب کنند، Action Scheduler خودش دوباره تلاش می‌کند. نمی‌کند. اکشن فقط failed می‌خورد و تمام؛ فقط اکشن‌های recurring دوباره زمان‌بندی می‌شوند. اگر روی این فرض حساب کنید، پیام‌ها بی‌صدا گم می‌شوند.
  • خطای موقت از دائمی جدا شده. خطای ۴۲۹ و ۵xx ارزش تلاش مجدد دارد؛ توکن غلط یا chat ID اشتباه ندارد و فقط صف را شلوغ می‌کند.
  • برش در مبدأ، نه روی رشته‌ی نهایی. دلیلش را در بخش بعد می‌بینید.

روش ۲: Google Apps Script (پلن B، بدون Cloudflare)

اگر با Cloudflare راه نیفتادید، همین منطق رله را می‌شود روی Google Apps Script پیاده کرد. افزونه‌ی Order and Stock Notifications via Telegram Bot for WooCommerce (نسخه‌ی ۱.۰.۳) هر دو حالت Apps Script و Cloudflare Workers را آماده دارد و سورس اسکریپت‌ها را در مخزن گیت‌هابش گذاشته. برای کسی که کد نمی‌زند، سریع‌ترین مسیر همین است.

روش ۳: پروکسی مستقیم روی سرور

اگر پروکسی خودتان را دارید، می‌توانید وردپرس را وادار کنید از آن استفاده کند. برای پروکسی HTTP، در wp-config.php:

define( 'WP_PROXY_HOST', '10.0.0.5' );
define( 'WP_PROXY_PORT', '3128' );
define( 'WP_PROXY_USERNAME', 'user' );
define( 'WP_PROXY_PASSWORD', 'pass' );

// Critical: keep local and Iranian services off the proxy.
define( 'WP_PROXY_BYPASS_HOSTS', 'localhost, *.ir, api.zarinpal.com, *.shaparak.ir' );

برای SOCKS5 که وردپرس بومی پشتیبانی نمی‌کند، باید مستقیم به cURL دست بزنید:

add_action( 'http_api_curl', function ( $handle, $args, $url ) {
    if ( false === strpos( $url, 'api.telegram.org' ) ) {
        return; // Only proxy Telegram traffic.
    }
    curl_setopt( $handle, CURLOPT_PROXY, '127.0.0.1' );
    curl_setopt( $handle, CURLOPT_PROXYPORT, 1080 );
    curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME );
}, 10, 3 );

آن strpos اختیاری نیست. بدونش کل ترافیک خروجی سایت از پروکسی رد می‌شود و درگاه پرداخت شما می‌افتد.


چه چیزهایی خراب می‌شود

این بخش را با دقت بخوانید. اینها همان چیزهایی است که دو هفته بعد از راه‌اندازی به سراغتان می‌آید.

checkout کند می‌شود یا می‌افتد. اگر ارسال پیام را مستقیم داخل هوک woocommerce_checkout_order_processed بگذارید، مشتری تا وقتی درخواست تلگرام جواب بدهد پشت صفحه‌ی سفید منتظر می‌ماند. اگر رله در دسترس نباشد، این انتظار به اندازه‌ی timeout طول می‌کشد. یعنی یک اختلال در Cloudflare مستقیم به نرخ تبدیل شما ضربه می‌زند. راه‌حل همان چیزی است که در کد بالا آمد: Action Scheduler یا 'blocking' => false.

پروکسی سراسری، درگاه پرداخت را می‌کشد. WP_PROXY_HOST روی همه‌ی درخواست‌های خروجی وردپرس اعمال می‌شود: به‌روزرسانی هسته، لایسنس افزونه‌ها، API پست و تیپاکس، و از همه بدتر درگاه پرداخت. اگر درخواست‌های شاپرکی شما از یک IP خارجی رد شوند، تراکنش‌ها fail می‌شوند. WP_PROXY_BYPASS_HOSTS را جدی بگیرید.

داده‌ی مشتری از سرور شخص ثالث رد می‌شود. بعضی افزونه‌های آماده برای حل همین مشکل فیلترینگ، یک سرور واسط اشتراکی گذاشته‌اند. نمونه‌اش Notify Bot for WooCommerce (نسخه‌ی ۲.۶.۱) که در بخش 3rd Party Services مستندات رسمی خودش با شفافیت نوشته اگر حالت پروکسی فعال باشد، درخواست‌ها از دامنه‌ی اختصاصی توسعه‌دهنده (tl.alijvhr.com) عبور می‌کنند.

توسعه‌دهنده این را برای راحتی کار گذاشته و پنهانش هم نکرده. ولی از نظر معماری، معنی‌اش این است که توکن بات، شماره‌ی تماس و آدرس مشتری‌های شما از سروری رد می‌شود که کنترلش دست شما نیست. برای تست خوب است؛ برای فروشگاهی که داده‌ی هویتی در پیام‌ها دارد، نه. در روش رله‌ی اختصاصی روی دامنه‌ی خودتان، این مسئله کلاً منتفی است.

نکته‌ی دوم درباره‌ی همین افزونه: در readme.txt مخزن، Tested up to روی وردپرس ۶.۸.۲ و WC tested up to روی ووکامرس ۱۰.۱.۲ مانده. یعنی سه نسخه‌ی اصلی عقب‌تر از وضعیت فعلی. قبل از نصب روی فروشگاه فعال، روی staging تستش کنید.

workers.dev فیلتر است. تکرارش می‌ارزد. Worker می‌سازید، از لپ‌تاپ با VPN تست می‌کنید و کار می‌کند، بعد روی سرور جواب نمی‌دهد. دامنه‌ی شخصی وصل کنید.

پیام تکراری. یک سفارش ممکن است چند بار وضعیت عوض کند: pending بعد processing بعد دوباره processing توسط درگاه. بدون قفل idempotency، گروه مدیران پر می‌شود از پیام تکراری و بعد از دو روز کسی دیگر نگاهش نمی‌کند.

سقف نرخ تلگرام. تلگرام روی ارسال به یک گروه حدود ۲۰ پیام در دقیقه سقف می‌گذارد. روز حراج، صف پیام‌ها با خطای 429 Too Many Requests برمی‌گردد و پیام‌ها از دست می‌روند. اگر ترافیک بالایی دارید، سفارش‌ها را در بازه‌های یک‌دقیقه‌ای دسته‌بندی و در یک پیام بفرستید.

Markdown می‌شکند. اسم محصولی که _ یا * یا [ دارد، با parse_mode: Markdown باعث خطای can't parse entities می‌شود و پیام اصلاً نمی‌رسد. HTML امن‌تر است، به شرطی که خروجی را esc_html کنید.

سقف ۴۰۹۶ کاراکتر و شکستن ساختار پیام. سفارش با ۳۰ قلم کالا از این سقف رد می‌شود و تلگرام کل پیام را دور می‌اندازد، نه اینکه برشش بزند. تله‌ی اصلی اما جای دیگری است: راه‌حل شهودی این است که با mb_substr متن را کوتاه کنید. اگر پیام HTML باشد، این کار خطرناک است. اگر برش دقیقاً وسط یک <b> یا <a href="..."> بیفتد، تگ باز می‌ماند و تلگرام با 400 Bad Request: can't parse entities کل پیام را رد می‌کند — یعنی دقیقاً همان اتفاقی که می‌خواستید جلویش را بگیرید، بدتر می‌شود.

راه درست این است که تعداد اقلام را در مبدأ محدود کنید (مثلاً ۱۲ قلم اول و بعد «و X قلم دیگر…») تا لینک پیشخوان و تگ‌ها همیشه سالم بمانند. همان کاری که در کد بالا با array_slice انجام شد.

گواهی‌های CA قدیمی. روی سرورهای ایرانی قدیمی، خطای cURL شماره‌ی ۶۰ رایج است. یعنی مسیر باز است ولی سرور نمی‌تواند گواهی طرف مقابل را تأیید کند. ca-certificates را به‌روز کنید؛ sslverify => false نگذارید.

WP-Cron خوابیده. Action Scheduler روی WP-Cron سوار است. اگر سایت شما ترافیک کم دارد یا DISABLE_WP_CRON روشن است، صف اجرا نمی‌شود و پیام‌ها با تأخیر می‌رسند. یک cron واقعی روی سرور ست کنید.

سیستم بی‌صدا می‌میرد. بدترین حالت این است: رله می‌افتد، سفارش‌ها می‌آیند، هیچ پیامی نمی‌رسد و شما فکر می‌کنید فروش نداشته‌اید. یک heartbeat روزانه بفرستید — یک پیام «سیستم زنده است» در ساعت مشخص. نبودِ آن پیام، خودش هشدار است.


سؤالات متداول

بدون سرور خارجی می‌شود ووکامرس را به تلگرام وصل کرد؟ بله. روش Cloudflare Workers یا Apps Script دقیقاً برای همین است. هیچ VPS خارجی نمی‌خواهد، فقط یک دامنه که DNS آن روی Cloudflare باشد.

خطای Failed to connect to api.telegram.org port 443 یعنی چه؟ یعنی سرور شما اصلاً به تلگرام نرسیده. توکن و chat ID را عوض نکنید؛ مشکل شبکه است. باید رله یا پروکسی راه بیندازید.

webhook تلگرام روی سرور ایران کار می‌کند؟ جهت ورودی معمولاً مشکل کمتری دارد، چون تلگرام به سرور شما وصل می‌شود نه برعکس. شرطش این است که دامنه SSL معتبر داشته باشد و روی یکی از پورت‌های مجاز تلگرام (۴۴۳، ۸۰، ۸۸، ۸۴۴۳) باشد. اگر هاست شما ترافیک ورودی خارجی را محدود کرده، جواب نمی‌دهد. ساده‌ترین کار این است که همان Worker را هم به‌عنوان مقصد webhook ست کنید و از آنجا به سایتتان forward کنید.

می‌شود از تلگرام وضعیت سفارش را عوض کرد؟ بله، ولی به مسیر برگشت هم نیاز دارید. با inline keyboard و webhook می‌شود دکمه‌ی «تأیید» و «لغو» زیر هر سفارش گذاشت. افزونه‌ی Notify Bot این را آماده دارد؛ برای نسخه‌ی اختصاصی، منطقش همان رله است با یک endpoint در سمت وردپرس.

فروش مستقیم از داخل تلگرام چطور؟ دو مدل دارد. مدل ساده: بات فقط لینک خرید محصول را می‌فرستد و پرداخت در سایت انجام می‌شود — پایدارترین گزینه برای ایران. مدل پیچیده: کل سبد خرید داخل بات ساخته و از طریق REST API ووکامرس ثبت می‌شود. مدل دوم نگهداری بیشتری می‌خواهد و با درگاه‌های ایرانی درگیری بیشتری دارد.

کدام نسخه‌ها؟ در زمان نوشتن این مقاله و طبق API رسمی مخزن وردپرس، آخرین نسخه‌ی WooCommerce ۱۱.۱.۰ است و حداقل وردپرس ۷.۰ و PHP ۷.۴ را الزامی می‌کند. نسخه‌ی جاری هسته‌ی وردپرس هم ۷.۱ است.

کد بالا با HPOS سازگار است، چون همه‌جا از wc_get_order و متدهای شیء $order استفاده می‌کند و مستقیم سراغ جدول post_meta نمی‌رود.


جمع‌بندی

مشکل اتصال ووکامرس به تلگرام از سرور ایران، مشکل افزونه نیست؛ مشکل مسیر خروج ترافیک است و با یک رله‌ی ساده حل می‌شود. بخش سختش نگهداری است: صف، جلوگیری از پیام تکراری، bypass درست پروکسی، و یک heartbeat که بفهمید کِی سیستم خوابیده.

اگر نمی‌خواهید خودتان درگیر نگهداری این زنجیره شوید، ما در hami9.ir همین مسیر را به‌صورت اختصاصی برای فروشگاه‌ها راه‌اندازی می‌کنیم — رله‌ی اختصاصی روی دامنه‌ی خودتان، workflow خودکار روی n8n برای گزارش‌های دوره‌ای، و پایش سلامت اتصال.