ربات تلگرام

انتخاب و راه اندازی هاست پایتون با cPanel برای جنگو و ربات تلگرام

هاست پایتون با cPanel و CloudLinux برای سایت جنگو، API سبک و ربات تلگرامی که با وبهوک کار می کند کافی است و به سرور مجازی نیاز ندارد. اپلیکیشن را در صفحه Setup Python App می سازید و بیشتر خطاهای دیپلوی به چند تنظیم مشخص جنگو و virtualenv برمی گردد.

نوشته تیم فنی پیکوهاستبه روزرسانی: ۲۳ شهریور ۱۴۰۵۱۴ دقیقه مطالعه

جنگو روی سیستم خودتان با python manage.py runserver بالا می آید، ولی روی هاست پایتون اشتراکی این دستور جایی ندارد. آنجا وب سرور درخواست را می گیرد و به اپلیکیشن WSGI شما تحویل می دهد. اگر بدانید این مسیر از کدام فایل ها می گذرد، علت خطای 500 یا پنل ادمینی را که بدون CSS باز می شود زودتر پیدا می کنید.

هاست پایتون در cPanel برنامه را چطور اجرا می کند

در هاست پایتون cPanel، اپلیکیشن را در صفحه Setup Python App می سازید و وب سرور آن را با دستورهای Passenger اجرا می کند. Setup Python App رابط Python Selector کلودلینوکس است و در بخش Software پیشخوان cPanel قرار دارد. با ساخت هر اپلیکیشن، سیستم یک virtualenv جدا برای آن درست می کند و چند دستور Passenger مثل PassengerAppRoot را در فایل .htaccess پوشه دامنه می نویسد. این خطوط را دستی ویرایش نکنید و تغییرات را از خود Setup Python App بدهید.

روی Apache این دستورها را Phusion Passenger اجرا می کند. LiteSpeed همان دستورها را می خواند، ولی برنامه را با پیاده سازی خودش اجرا می کند. در هر دو حالت نقطه شروع برنامه فایل passenger_wsgi.py است که باید یک شیء WSGI در اختیار وب سرور بگذارد.

عمر پروسس برنامه دست وب سرور است. پروسس پایتون با رسیدن درخواست ساخته می شود و وب سرور ممکن است پروسسی را که مدتی بیکار مانده ببندد، برای همین اولین درخواست بعد از بیکاری کمی دیرتر جواب می گیرد. کدی که باید بی وقفه در پس زمینه بچرخد، مثل حلقه long polling ربات تلگرام، روی این پروسس ها قابل اتکا نیست.

هاست پایتون برای جنگو چه امکاناتی لازم دارد

برای جنگو چهار چیز را در هاست پایتون چک کنید: نسخه های پایتونی که در Setup Python App قابل انتخاب است، نوع و نسخه دیتابیس، دسترسی ترمینال یا SSH برای اجرای pip، migrate و collectstatic، و امکان تعریف کرون جاب. این ها را قبل از خرید از فروشنده بپرسید یا در مشخصات سرویس پیدا کنید. نسخه دیتابیس را دقیق تر بررسی کنید، چون جنگو برای MySQL، MariaDB و PostgreSQL حداقل نسخه دارد و به دیتابیس قدیمی تر وصل نمی شود.

جدول زیر حداقل ها را برای جنگو 5.2 (نسخه LTS) و جنگو 6.1 (تازه ترین نسخه) نشان می دهد. نسخه های پایتون از جدول سازگاری پایتون در مستندات جنگو و نسخه های دیتابیس از یادداشت های دیتابیس جنگو 5.2 و جنگو 6.1 آمده است.

نسخه جنگونسخه پایتونMySQLMariaDBPostgreSQL
5.2 (LTS)3.10 تا 3.148.0.11 و بالاتر10.5 و بالاتر14 و بالاتر
6.13.12 تا 3.148.4 و بالاتر10.11 و بالاتر15 و بالاتر

نسخه دیتابیس سرور را با کوئری SELECT VERSION(); در phpMyAdmin یا phpPgAdmin ببینید. اگر مثلاً سرور MariaDB 10.6 دارد، جنگو 6.1 به آن وصل نمی شود ولی 5.2 مشکلی ندارد؛ در این حالت نسخه را در requirements.txt با Django>=5.2,<5.3 ثابت کنید.

پیکوهاست این سرویس را در دو لوکیشن ارائه می کند: هاست پایتون آلمان و هاست پایتون هلند. هر دو دسترسی ترمینال، چند نسخه پایتون، دیتابیس MySQL و PostgreSQL و گواهی SSL رایگان دارند و سرورها با CloudLinux و LiteSpeed کار می کنند. اگر نسخه دقیق پایتون یا دیتابیس برایتان مهم است، قبل از خرید تیکت بدهید؛ مشاوره پیش از خرید رایگان است.

ساخت اپلیکیشن در Setup Python App

  1. در cPanel وارد Setup Python App شوید و Create Application را بزنید.
  2. در Python version نسخه ای را انتخاب کنید که با نسخه جنگو پروژه تان جور باشد.
  3. در Application root نام پوشه برنامه را نسبت به پوشه home بنویسید، مثلاً myproject. این پوشه را داخل public_html نسازید تا کد و فایل تنظیمات از مرورگر قابل دانلود نباشد.
  4. در Application URL دامنه یا ساب دامینی را که برنامه باید رویش باز شود انتخاب کنید.
  5. در Application startup file بنویسید passenger_wsgi.py و در Application Entry point بنویسید application.
  6. در Passenger log file مسیری مثل /home/USERNAME/logs/myproject.log بدهید تا خطاهای برنامه در یک فایل مشخص جمع شود.
  7. Create را بزنید.

بعد از ساخت، بالای صفحه اپلیکیشن دستوری برای ورود به virtualenv نمایش داده می شود. آن را کپی کنید و در Terminal خود cPanel یا از طریق SSH اجرا کنید. شکل کلی اش این است:

source /home/USERNAME/virtualenv/myproject/3.12/bin/activate && cd /home/USERNAME/myproject

هر pip install و هر دستور manage.py را بعد از این دستور بزنید. پکیجی که بیرون از virtualenv اپلیکیشن نصب شود برای برنامه وجود ندارد و نتیجه اش خطای ModuleNotFoundError است.

دیپلوی جنگو روی cPanel قدم به قدم

۱. آپلود کد

کد را به صورت zip با File Manager داخل Application root آپلود و از حالت فشرده خارج کنید. آموزش تصویری آپلود سورس در cPanel این کار را با تصویر نشان می دهد. اگر git روی سرور در دسترس است، مخزن را در ترمینال با git clone بگیرید. پوشه virtualenv محلی، __pycache__ و db.sqlite3 را آپلود نکنید.

۲. نصب وابستگی ها

فایل requirements.txt را روی سیستم خودتان با pip freeze > requirements.txt بسازید و مطمئن شوید نسخه جنگو در آن ثابت شده. بعد روی سرور، داخل virtualenv:

pip install -r requirements.txt

۳. فایل passenger_wsgi.py

Setup Python App هنگام ساخت اپلیکیشن یک passenger_wsgi.py نمونه می سازد. محتوای آن را با کد زیر عوض کنید و myproject را به نام پوشه ای که settings.py داخلش است تغییر دهید:

import os
import sys

sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

from myproject.wsgi import application

۴. تنظیمات production

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]

SECRET_KEY و رمز دیتابیس را داخل کد ننویسید. صفحه اپلیکیشن در Setup Python App بخش Environment variables دارد؛ متغیرها را آنجا تعریف کنید و برنامه را ری استارت کنید. این متغیرها به برنامه ای می رسند که وب سرور اجرا می کند. اگر manage.py در ترمینال با KeyError روی یکی از آن ها متوقف شد، همان مقدارها را در فایل .env داخل Application root بگذارید، هر خط به شکل DJANGO_SECRET_KEY='...' و مقدار داخل کوتیشن تکی. دسترسی فایل را با chmod 600 .env محدود کنید و قبل از دستورهای manage.py بارش کنید:

set -a && . ./.env && set +a

بعد python manage.py check --deploy را اجرا کنید تا جنگو تنظیمات ناامن را فهرست کند. این دستور بخشی از چک لیست رسمی دیپلوی جنگو است.

روی هاست، جنگو فایل های استاتیک را خودش سرو نمی کند؛ runserver این کار را فقط روی سیستم خودتان و در حالت DEBUG انجام می داد. ALLOWED_HOSTS هم باید دامنه را داشته باشد، وگرنه جنگو به درخواست ها خطای 400 می دهد. دو خطای رایج در جدول پایین از همین دو تنظیم می آیند.

۵. دیتابیس و migrate

در cPanel از بخش MySQL Databases یا PostgreSQL Databases دیتابیس و کاربر بسازید و کاربر را با همه دسترسی ها به دیتابیس اضافه کنید. cPanel نام کاربری اکانت را با یک زیرخط اول نام دیتابیس و نام کاربر دیتابیس می گذارد و در تنظیمات باید همین نام کامل را بنویسید:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "USERNAME_mydb",
        "USER": "USERNAME_dbuser",
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": "localhost",
        "OPTIONS": {"charset": "utf8mb4"},
    }
}

درایور پیشنهادی جنگو برای MySQL پکیج mysqlclient است که هنگام نصب کامپایل می شود و به هدرهای کلاینت MySQL نیاز دارد. اگر نصبش روی هاست شکست خورد، راه حل در جدول خطاها آمده. برای PostgreSQL موتور را django.db.backends.postgresql بگذارید و درایور را با pip install "psycopg[binary]" نصب کنید؛ نسخه binary کامپایل لازم ندارد.

بعد از تنظیم دیتابیس:

python manage.py migrate
python manage.py createsuperuser

۶. ری استارت برنامه

بعد از هر تغییر در کد، تنظیمات یا متغیرهای محیطی، در صفحه اپلیکیشن دکمه Restart را بزنید. تا ری استارت نکنید، پروسس قبلی با کد قدیمی به کار ادامه می دهد. راه دیگر، ساختن یا به روز کردن فایل tmp/restart.txt داخل Application root است که Passenger و LiteSpeed هر دو آن را می شناسند:

mkdir -p tmp && touch tmp/restart.txt

فایل های استاتیک و مدیا در هاست جنگو

روی هاست cPanel ساده ترین راه سرو کردن فایل های استاتیک جنگو پکیج WhiteNoise است، چون به تنظیم وب سرور نیاز ندارد. آن را با pip install whitenoise نصب کنید، به requirements.txt اضافه کنید و این تنظیمات را در settings.py بگذارید. طبق مستندات WhiteNoise، middleware آن باید درست بعد از SecurityMiddleware بیاید:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # بقیه middlewareها بدون تغییر
]

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

بعد python manage.py collectstatic --noinput را اجرا کنید و برنامه را ری استارت کنید، چون WhiteNoise فهرست فایل ها را هنگام شروع برنامه می خواند. هر بار که فایل استاتیکی عوض می شود یا پکیجی با فایل استاتیک نصب می کنید، این دو کار را تکرار کنید.

WhiteNoise برای فایل هایی که کاربران آپلود می کنند مناسب نیست. برای مدیا، MEDIA_ROOT را پوشه ای داخل document root دامنه بگذارید، مثلاً /home/USERNAME/public_html/media، و MEDIA_URL را media/. بعد یک فایل آپلود کنید و آدرسش را در مرورگر باز کنید تا مطمئن شوید وب سرور آن را مستقیم برمی گرداند. اگر Application URL روی ساب دامین است، document root آن ساب دامین را در بخش Domains در cPanel ببینید. چون این پوشه داخل document root است، پسوند فایل هایی را که کاربران آپلود می کنند در جنگو محدود کنید، مثلاً با FileExtensionValidator، تا فایل PHP یا اسکریپت دیگری آنجا نرسد.

اجرای ربات تلگرام پایتون روی هاست

روی هاست پایتون اشتراکی، ربات تلگرام باید آپدیت ها را با وبهوک بگیرد. طبق مستندات Bot API تلگرام، ربات آپدیت ها را یا با متد getUpdates (همان long polling) می گیرد یا با وبهوک، و تا وقتی وبهوک تنظیم شده باشد getUpdates کار نمی کند.

چرا long polling روی هاست اشتراکی کار نمی کند

در long polling اسکریپت در یک حلقه دائمی از تلگرام آپدیت می خواهد، پس پروسسش باید همیشه روشن بماند. روی هاست اشتراکی پروسس ها برای جواب دادن به درخواست های وب ساخته می شوند و وب سرور می تواند آن ها را ببندد. اگر حلقه polling را داخل خود اپلیکیشن راه بیندازید، با بسته شدن پروسس ربات هم از کار می افتد. اگر هم وب سرور دو پروسس هم زمان بسازد، دو نسخه ربات getUpdates صدا می زنند و تلگرام خطای 409 Conflict برمی گرداند.

اجرای اسکریپت با nohup از ترمینال هم راه حل نیست. CloudLinux مصرف منابع و تعداد پروسس های هر اکانت را محدود می کند و پروسسی که بیرون از وب سرور می چرخد تضمینی برای زنده ماندن ندارد.

وبهوک از داخل یک view جنگو

با وبهوک، تلگرام هر آپدیت را به صورت یک درخواست POST به آدرس HTTPS شما می فرستد. این درخواست را view جنگو مثل هر درخواست وب دیگری جواب می دهد، پس با مدل اجرای هاست جور است. روی هاست اشتراکی نمی توانید پورت دلخواه باز کنید و آدرس وبهوک باید یکی از مسیرهای همان اپلیکیشن باشد. اپ bot را با python manage.py startapp bot بسازید، به INSTALLED_APPS اضافه کنید و این view ساده را که متن پیام کاربر را برمی گرداند در آن بگذارید:

# bot/views.py
import hmac
import json
import os

import requests
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

TOKEN = os.environ["BOT_TOKEN"]
SECRET = os.environ["WEBHOOK_SECRET"]


@csrf_exempt
@require_POST
def telegram_webhook(request):
    received = request.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
    if not hmac.compare_digest(received.encode(), SECRET.encode()):
        return HttpResponseForbidden()
    update = json.loads(request.body)
    message = update.get("message")
    if message and "text" in message:
        requests.post(
            f"https://api.telegram.org/bot{TOKEN}/sendMessage",
            json={"chat_id": message["chat"]["id"], "text": message["text"]},
            timeout=10,
        )
    return HttpResponse("ok")
# myproject/urls.py
from django.contrib import admin
from django.urls import path

from bot.views import telegram_webhook

urlpatterns = [
    path("admin/", admin.site.urls),
    path("telegram/webhook/", telegram_webhook),
]

requests را به requirements.txt اضافه کنید، BOT_TOKEN و WEBHOOK_SECRET را در Environment variables بگذارید و برنامه را ری استارت کنید. بعد وبهوک را ثبت کنید:

curl -F "url=https://example.com/telegram/webhook/" \
     -F "secret_token=YOUR_SECRET" \
     "https://api.telegram.org/botYOUR_TOKEN/setWebhook"

تلگرام مقدار secret_token را در هدر X-Telegram-Bot-Api-Secret-Token هر درخواست می فرستد و view بالا درخواستی را که این مقدار را ندارد رد می کند. طول این مقدار ۱ تا ۲۵۶ کاراکتر است و فقط حروف انگلیسی، عدد، _ و - در آن مجاز است. آدرس وبهوک باید HTTPS باشد، پس قبل از ثبت، SSL دامنه را فعال کنید. اگر view کدی غیر از 2xx برگرداند، تلگرام همان آپدیت را چند بار دوباره می فرستد؛ view را سبک نگه دارید و کار طولانی را در آن انجام ندهید.

پاک کردن وبهوک قبلی و خواندن خطاها با getWebhookInfo در آموزش تنظیم وبهوک ربات تلگرام آمده است. اگر هنوز بین هاست PHP، پایتون و میدلاین مردد هستید، راهنمای انتخاب هاست ربات تلگرام را بخوانید.

کارهای زمان بندی شده با کرون جاب

حلقه while True با time.sleep روی هاست اشتراکی همان مشکل long polling را دارد. برای کارهای دوره ای، مثل پاک کردن سشن های منقضی یا ارسال گزارش روزانه ربات، یک management command جنگو بنویسید و آن را با کرون اجرا کنید. کرون virtualenv را فعال نمی کند و متغیرهای Setup Python App را هم ندارد، پس مسیر کامل پایتون virtualenv را بنویسید و فایل .env را قبل از دستور بار کنید:

0 3 * * * cd /home/USERNAME/myproject && set -a && . ./.env && set +a && /home/USERNAME/virtualenv/myproject/3.12/bin/python manage.py clearsessions >> /home/USERNAME/logs/cron.log 2>&1

این خط هر روز ساعت ۳ بامداد به وقت سرور clearsessions را اجرا می کند و خروجی و خطاهایش را در فایل لاگ می نویسد. تعریف کرون در cPanel را در آموزش تنظیم کرون جاب در cPanel ببینید. کمترین فاصله اجرای کرون یک دقیقه است و کاری که باید هر چند ثانیه انجام شود جایش روی هاست اشتراکی نیست.

خطاهای رایج دیپلوی جنگو و راه رفع آن ها

برای هر خطای 500 اول فایلی را بخوانید که در Passenger log file تعریف کرده اید. اگر خطا هنگام import رخ می دهد، داخل virtualenv و در Application root دستور python -c "import passenger_wsgi" را بزنید تا traceback کامل را در ترمینال ببینید.

نشانهعلت معمولراه رفع
خطای 500 یا صفحه خطای وب سرور بعد از جایگزین کردن passenger_wsgi.pyخطا در passenger_wsgi.py، نام اشتباه ماژول settings یا Entry pointلاگ Passenger را بخوانید و python -c "import passenger_wsgi" را اجرا کنید
ModuleNotFoundError: No module named 'django'پکیج ها بیرون از virtualenv یا برای نسخه دیگری از پایتون نصب شده انددستور ورود به virtualenv را از صفحه اپلیکیشن اجرا کنید و pip install -r requirements.txt را دوباره بزنید
ModuleNotFoundError: No module named 'myproject'پوشه پروژه در sys.path نیست یا Application root به پوشه اشتباهی اشاره می کندخط sys.path.insert را در passenger_wsgi.py بگذارید و مسیر Application root را چک کنید
KeyError: 'DJANGO_SECRET_KEY' هنگام اجرای manage.py در ترمینال یا کرونمتغیرهای Setup Python App به شل و کرون نمی رسندفایل .env را با set -a && . ./.env && set +a قبل از دستور بار کنید
خطای 400 و DisallowedHost با پیغام Invalid HTTP_HOST headerدامنه یا نسخه www آن در ALLOWED_HOSTS نیستهر دو شکل دامنه را اضافه کنید و ری استارت کنید
صفحه ها و پنل ادمین بدون CSS، فایل های /static/ با 404collectstatic اجرا نشده یا کسی STATIC_ROOT را سرو نمی کندWhiteNoise را تنظیم کنید، collectstatic را اجرا کنید و ری استارت کنید
pip install mysqlclient هنگام ساخت wheel با خطایی درباره pkg-config متوقف می شودهدرهای کلاینت MySQL یا ابزار ساخت روی سرور در دسترس نیستاز پشتیبانی بپرسید هدرها نصب هستند یا نه، یا دیتابیس را PostgreSQL با psycopg[binary] کنید
NotSupportedError با پیغام ... or later is requiredنسخه دیتابیس سرور از حداقل نسخه جنگو پایین تر استجنگو را روی نسخه ای ثابت کنید که با دیتابیس سرور سازگار است
تغییرات کد روی سایت دیده نمی شودپروسس قدیمی هنوز در حال اجراستRestart را در Setup Python App بزنید
Conflict: terminated by other getUpdates requestدو نسخه ربات هم زمان long polling می کنندpolling را کنار بگذارید و وبهوک تنظیم کنید
Conflict: can't use getUpdates method while webhook is activeوبهوک فعال است و اسکریپتی هنوز getUpdates صدا می زنداسکریپت polling را متوقف کنید یا اگر عمداً polling می خواهید، اول deleteWebhook را بزنید

چه وقت از هاست اشتراکی به سرور مجازی بروید

وقتی برنامه به پروسس همیشه روشن، دسترسی root یا پورت اختصاصی نیاز دارد، هاست پایتون اشتراکی جواب نمی دهد و باید سرور مجازی بگیرید. نمونه های رایج:

  • ربات long polling، worker صف مثل Celery یا کلاینت MTProto که باید بی وقفه روشن بمانند.
  • WebSocket و Django Channels که به یک سرور ASGI همیشه روشن نیاز دارند.
  • پکیج سیستمی یا کتابخانه ای که نصبش دسترسی root می خواهد.
  • پورت اختصاصی یا سرویس هایی که کنار برنامه اجرا می شوند، مثل Redis.

روی سرور مجازی، وب سرور، gunicorn یا uvicorn و systemd را خودتان تنظیم می کنید و عمر پروسس ها دست شماست. برای ربات تلگرام محل سرور هم مهم است، چون دسترسی به سرورهای تلگرام از داخل ایران محدود است. سرور مجازی هلند پیکوهاست در دیتاسنتر آمستردام و با مجازی سازی KVM ارائه می شود. اگر پروژه جنگو شما سایتی برای کاربران داخل ایران است و به تلگرام وصل نمی شود، سرور مجازی ایران در دیتاسنترهای تهران با دسترسی root کامل هم گزینه ای است. این دو را در مقایسه سرور مجازی ایران و خارج کنار هم گذاشته ایم.

چک لیست بعد از هر دیپلوی

هر بار که کد جدید روی هاست می گذارید:

  1. کد جدید را آپلود یا git pull کنید.
  2. وارد virtualenv شوید، اگر از .env استفاده می کنید آن را بار کنید و pip install -r requirements.txt را بزنید.
  3. python manage.py migrate و python manage.py collectstatic --noinput را اجرا کنید.
  4. در Setup Python App دکمه Restart را بزنید.
  5. python manage.py check --deploy را اجرا کنید و هشدارها را بخوانید.
  6. صفحه اصلی و پنل ادمین را در مرورگر باز کنید و فایل لاگ Passenger را نگاه کنید.
  7. برای ربات، getWebhookInfo را صدا بزنید و فیلد last_error_message را چک کنید.