الأدلة التقنية

دليل Asterisk العملي: من التحويلة Extension إلى الرد الآلي IVR

نبني مقسم Asterisk من الكود المصدري في Docker، ونكتب خطة اتصال فيها بريد صوتي ورد آلي وتسجيل مكالمات، ثم نتحكم به عبر AMI وARI.

دليل Asterisk العملي: من التحويلة Extension إلى الرد الآلي IVR

في مقال تتبع المكالمة استخدمنا Asterisk كأداة فقط: تحويلتان وسطران في خطة الاتصال، ثم ركزنا على ما يحدث للحزم في الشبكة. ولكن المقسم في أي مكتب حقيقي يطلب منه أكثر من ذلك بكثير، فالموظف الذي لا يرد يجب أن يترك له المتصل رسالة، والعميل الذي يتصل بالرقم الرئيسي يسمع قائمة «للمبيعات اضغط 1»، والإدارة تريد تسجيل المكالمات، ونظام CRM يريد أن يعرف من يتصل الآن وأن يبدأ مكالمة بضغطة زر. وكل هذا يكتب في Asterisk في ملفات نصية، وأغلب من يفتحها أول مرة يغلقها بسرعة، والسبب أن الوثائق ضخمة ولا تقول لك من أين تبدأ.

المواضيع في السلسلة:

لذلك سوف نبني في هذا المقال مقسماً كاملاً خطوة بخطوة، وكل خطوة تبنى على التي قبلها، وكل مخرج في المقال جاء من تشغيل حقيقي. وسوف نتناول ما يلي:

  • ما هو Asterisk، ومتى تختاره ومتى تختار غيره، وأي إصدار تثبت بحسب سياسة الدعم الطويل LTS.
  • بناء Asterisk 22 من الكود المصدري في Docker Image، وتشغيله مع Docker Compose.
  • التحويلات Extensions في PJSIP بالقوالب Templates.
  • أساسيات خطة الاتصال Dialplan، ثم تسجيل المكالمات Call Recording والبريد الصوتي Voicemail.
  • رد آلي IVR بقائمة ووقت دوام ومنطقة زمنية.
  • التحكم بالمقسم من برامجك عبر AMI وARI، مع تجربة لكل منهما.
  • التشخيص من سطر الأوامر CLI، والنسخ الاحتياطي والتحديث، وما يجب أن تغيره قبل الإنتاج.

ما هو Asterisk، ومتى تختاره؟

Asterisk محرك اتصالات مفتوح المصدر يعمل على Linux، يستقبل المكالمات من الهواتف والخطوط الخارجية، ثم ينفذ عليها ما تكتبه له في خطة الاتصال، سواءً كان ذلك تحويل المكالمة إلى هاتف آخر أو تشغيل رسالة أو تسجيل بريد صوتي أو تسليمها لبرنامج تكتبه أنت. وهو مرخص بترخيص GPL الإصدار الثاني كما في ملف الترخيص، وتملك حقوقه شركة Sangoma التي تبيع أيضاً ترخيصاً تجارياً لمن لا يناسبه GPL.

وقد يتساءل البعض: لماذا نتعب أنفسنا بملفات نصية ما دام FreePBX يقدم واجهة ويب فوق Asterisk نفسه؟ والإجابة على ذلك أن FreePBX يكتب هذه الملفات نيابة عنك، فإذا لم تفهم ما يكتبه فلن تستطيع تشخيص أي عطل فيه، ولن تستطيع إضافة ما لا توفره الواجهة. لذلك فهذا المقال هو الأساس، والمقال التالي في السلسلة عن FreePBX يبني عليه. وبشكل عام:

  • اختر Asterisk مباشرة إذا كان المقسم جزءاً من منتج تبنيه، أو تريد التحكم الكامل بخطة الاتصال، أو ستربطه ببرامجك عبر ARI، أو تريد أن تحفظ الإعداد كله في Git.
  • اختر FreePBX إذا كان من سيدير المقسم موظف دعم فني يضيف التحويلات ويغير المسارات من المتصفح.
  • انظر إلى FreeSWITCH أو Kamailio أمام عدة سيرفرات Asterisk إذا كنت تبني منصة لآلاف المكالمات في الوقت نفسه، وسوف نتناولهما في مقالات هذه السلسلة.

أي إصدار تثبت؟ سياسة الدعم الطويل LTS

يصدر مشروع Asterisk فرعاً رئيسياً جديداً مرة في السنة تقريباً، ويتناوب بين فرع عادي Standard وفرع بدعم طويل LTS. وبحسب جدول إصدارات Asterisk يحصل فرع LTS على أربع سنوات من الدعم الكامل ثم سنة من إصلاحات الأمان فقط، أما الفرع العادي فيحصل على سنة دعم كامل على الأقل ثم سنة إصلاحات أمان. والجدول التالي يبين الفروع المهمة كما في الصفحة الرسمية في 20 أكتوبر 2026:

الفرعالنوعالإصدار الأولإصلاحات الأمان فقطنهاية الدعم EOLالحالة
20LTS2022-10-192026-10-192027-10-19دخل مرحلة إصلاحات الأمان
21Standard2023-10-182025-10-182026-10-18انتهى دعمه
22LTS2024-10-162028-10-162029-10-16دعم كامل
23Standard2025-10-152026-10-152027-10-15دعم كامل بحسب الجدول
24LTS2026-10-15 (مخطط)2030-10-152031-10-15إصدار تجريبي RC

ولاحظ أن الفرع 24 موعده في الجدول 15 أكتوبر 2026، ولكن آخر ما نشر منه في صفحة التنزيل عند كتابة المقال هو 24.0.0-rc2 بتاريخ 8 أكتوبر، أي أنه لم يصدر بشكل نهائي بعد. لذلك اخترنا في هذا الدليل الإصدار 22.11.0، وهو آخر إصدار نهائي من فرع 22 LTS، صدر في 27 أغسطس 2026 كما في سجل التغييرات، ودعمه الكامل مستمر حتى أكتوبر 2028. والقاعدة التي ننصح بها هي ألا تقوم بتثبيت فرع LTS جديد في الإنتاج في أول أسابيعه، والسبب أن الإصدارات الأولى من كل فرع تحمل أغلب الأخطاء، فابدأ اليوم بفرع 22، وخطط للانتقال إلى 24 بعد عدة إصدارات تصحيحية منه.

كيف يعمل Asterisk من الداخل

قبل أن نكتب أي ملف، لنفهم الأجزاء التي سوف نتعامل معها، والصورة التالية تبين ما يحدث داخل Asterisk عندما تصل المكالمة:

هاتف مكتبي وهاتف برمجي وخط خارجي ترسل مكالماتها إلى برامج تشغيل القنوات في Asterisk مثل chan_pjsip، فتتحول كل مكالمة إلى قناة Channel، ثم تمر إلى النواة Core التي فيها خطة الاتصال والجسور Bridges والترميز، والنواة تستدعي التطبيقات مثل Dial وVoiceMail وMixMonitor وBackGround وQueue وStasis، وعلى اليمين ثلاث طرق للتحكم: سطر الأوامر CLI وAMI على المنفذ 5038 وARI عبر HTTP وWebSocket على 8088
القناة هي المكالمة من جهة واحدة، وخطة الاتصال تقرر ما يحدث لها
  • القناة Channel: كل طرف في المكالمة قناة مستقلة، فالمكالمة من Alice إلى Bob قناتان، الأولى PJSIP/1001-... من هاتف Alice إلى المقسم، والثانية PJSIP/1002-... من المقسم إلى هاتف Bob، وهذا هو دور B2BUA الذي شرحناه في مقال المصطلحات.
  • برنامج تشغيل القنوات Channel Driver: الوحدة التي تتكلم البروتوكول، والمستخدمة اليوم لـ SIP هي chan_pjsip، أما chan_sip القديمة فقد حذفت من Asterisk في الفرع 21 كما في سجل تغييرات 21.0.0.
  • خطة الاتصال Dialplan: التعليمات التي تقرر ماذا يحدث للقناة، في الملف extensions.conf.
  • التطبيقات Applications: الأفعال التي تستدعيها خطة الاتصال، مثل Dial للرنين وVoiceMail للبريد الصوتي.
  • الجسر Bridge: عندما يرد Bob يضع Asterisk القناتين في جسر واحد فيمر الصوت بينهما.
  • طرق التحكم: سطر الأوامر CLI لك أنت، وAMI وARI لبرامجك، وسوف نجرب الثلاثة.

المتطلبات

  • سيرفر Linux عليه Docker وDocker Compose، ويكفي للتجربة ولمكتب صغير معالجان وذاكرة 2 GB، والبناء من الكود المصدري يحتاج وقتاً أطول على الأجهزة الضعيفة.
  • مساحة قرص للتسجيلات والبريد الصوتي، وكل دقيقة تسجيل بصيغة WAV تأخذ نحو 1 MB كما سنرى.
  • للإنتاج: منفذ SIP 5060/UDP ونطاق منافذ RTP مفتوحان للهواتف فقط، أما التجربة في هذا المقال فتعمل كلها داخل شبكة Docker داخلية دون أي منفذ مفتوح على جهازك.

بناء Docker Image من الكود المصدري

لا توجد Image رسمية من مشروع Asterisk على Docker Hub، وحزم التوزيعات إما قديمة أو غير موجودة، ففي مقال تتبع المكالمة كانت حزمة Ubuntu 24.04 على الإصدار 20.6.0، أما Debian 12 فلا يوجد فيه حزمة asterisk في مستودعاته الرئيسية أصلاً. والطريقة التي توصي بها وثائق التثبيت هي البناء من الكود المصدري، لذلك سوف نبني Image من مرحلتين Multi-stage: الأولى فيها أدوات البناء، والثانية فيها البرنامج الناتج فقط. قم بإنشاء مجلد للمشروع فيه المجلد build، وضع فيه الملف build/Dockerfile:

FROM debian:12.15 AS build
ARG AST=22.11.0
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      build-essential wget ca-certificates bzip2 patch pkg-config \
      libedit-dev uuid-dev libjansson-dev libxml2-dev libsqlite3-dev libssl-dev libsrtp2-dev libcurl4-openssl-dev
WORKDIR /usr/src
RUN wget -q https://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-${AST}.tar.gz \
 && wget -q https://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-${AST}.sha256 \
 && sha256sum -c asterisk-${AST}.sha256 \
 && tar xzf asterisk-${AST}.tar.gz
WORKDIR /usr/src/asterisk-${AST}
RUN ./configure --with-pjproject-bundled --with-jansson-bundled=no > /dev/null \
 && make menuselect.makeopts \
 && menuselect/menuselect --disable BUILD_NATIVE \
      --enable CORE-SOUNDS-EN-ULAW --enable EXTRA-SOUNDS-EN-ULAW menuselect.makeopts
RUN make third-party > /tmp/tp.log 2>&1 || (tail -40 /tmp/tp.log; exit 1)
RUN make -j"$(nproc)" > /tmp/make.log 2>&1 || (tail -80 /tmp/make.log; exit 1)
RUN make install DESTDIR=/opt/ast > /dev/null \
 && make samples DESTDIR=/opt/ast > /dev/null \
 && rm -rf /opt/ast/var/run

FROM debian:12.15
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      libedit2 libuuid1 libjansson4 libxml2 libsqlite3-0 libssl3 libsrtp2-1 libcurl4 ca-certificates \
 && rm -rf /var/lib/apt/lists/* \
 && useradd -r -d /var/lib/asterisk -s /usr/sbin/nologin asterisk
COPY --from=build /opt/ast/ /
RUN ldconfig \
 && mkdir -p /var/spool/asterisk/monitor /var/run/asterisk /var/log/asterisk /var/lib/asterisk \
 && chown -R asterisk:asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk /var/run/asterisk /etc/asterisk
USER asterisk
CMD ["asterisk", "-f", "-vvv"]

في الإعداد أعلاه لاحظ التالي:

  • نتحقق من الملف المنزل بالأمر sha256sum -c مقابل الملف الذي ينشره المشروع بجانبه، فإذا تغير الملف في الطريق يتوقف البناء.
  • --with-pjproject-bundled يجعل Asterisk يبني نسخته المختبرة من مكتبة pjproject التي تقوم عليها وحدة PJSIP، بدل نسخة التوزيعة، وهي الطريقة التي توصي بها الوثائق.
  • الأداة menuselect تختار ما يبنى من الوحدات، كما في وثائق menuselect. وقد أطفأنا BUILD_NATIVE حتى لا يرتبط البرنامج بمعالج الجهاز الذي بني عليه، وإلا قد لا يعمل على سيرفر بمعالج أقدم، وأضفنا ملفات الصوت الإنجليزية بصيغة ulaw التي سنستخدمها في الرد الآلي.
  • بنينا third-party أولاً دون تشغيل متواز، والسبب أن بناء pjproject مع make -j فشل عندنا مرتين بالخطأ Makefile:212: source/pjsip/lib/libpjsua-x86_64-pc-linux-gnu.a] Error 2، ونجح عندما بنيناه وحده، ثم بنينا بقية Asterisk بالتوازي.
  • make samples يضع ملفات الإعداد النموذجية في /etc/asterisk، وسوف نستبدل منها ما نحتاجه فقط.
  • Asterisk يعمل بالمستخدم asterisk وليس root، فإذا وجد مخترق ثغرة فيه لا يحصل على صلاحيات ال Container كاملة.

نبني ال Image ونتأكد من الإصدار:

docker build -t arb-asterisk:22.11.0 build
docker run --rm arb-asterisk:22.11.0 asterisk -V
docker image ls arb-asterisk:22.11.0 --format '{{.Size}}'

والمخرج سوف يكون كما يلي:

Asterisk 22.11.0
692MB

والحجم كبير نسبياً لأن ال Image تحمل ملفات الصوت وملفات الإعداد النموذجية والوثائق المدمجة، وهذا مقبول لسيرفر واحد.

التشغيل مع Docker Compose

الآن نكتب ملفات الإعداد في المجلد conf، ونربط كل ملف منها داخل ال Container بدل نسخ المجلد كاملاً، حتى تبقى بقية الملفات النموذجية كما هي. وهذا هو الملف docker-compose.yml:

name: arb-ast
services:
  asterisk:
    build: ./build
    image: arb-asterisk:22.11.0
    container_name: arb-ast-pbx
    restart: unless-stopped
    networks:
      voip:
        ipv4_address: 10.88.0.10
    volumes:
      - ./conf/asterisk.conf:/etc/asterisk/asterisk.conf:ro
      - ./conf/pjsip.conf:/etc/asterisk/pjsip.conf:ro
      - ./conf/extensions.conf:/etc/asterisk/extensions.conf:ro
      - ./conf/voicemail.conf:/etc/asterisk/voicemail.conf:ro
      - ./conf/rtp.conf:/etc/asterisk/rtp.conf:ro
      - ./conf/logger.conf:/etc/asterisk/logger.conf:ro
      - ./conf/manager.conf:/etc/asterisk/manager.conf:ro
      - ./conf/http.conf:/etc/asterisk/http.conf:ro
      - ./conf/ari.conf:/etc/asterisk/ari.conf:ro
      - ./prompts:/var/lib/asterisk/sounds/custom:ro
      - spool:/var/spool/asterisk
      - logs:/var/log/asterisk
    healthcheck:
      test: ["CMD", "asterisk", "-rx", "core show uptime"]
      interval: 30s
      timeout: 5s
      retries: 3

networks:
  voip:
    name: arb-ast-net
    internal: true
    ipam:
      config:
        - subnet: 10.88.0.0/24

volumes:
  spool:
  logs:

في الإعداد أعلاه لاحظ التالي:

  • ال Volume المسمى spool يحفظ البريد الصوتي والتسجيلات وقاعدة AstDB، وهي أهم بيانات المقسم بعد ملفات الإعداد، وسوف نشرح سبب وضع AstDB فيه بعد قليل.
  • الشبكة internal: true لا طريق لها إلى خارج الجهاز، والهواتف في التجربة Containers على الشبكة نفسها، وهذا مناسب للتجربة فقط. أما في الإنتاج فتحتاج الهواتف أن تصل إلى المنفذ 5060 ونطاق RTP كاملاً، ونشر آلاف منافذ UDP بخيار ports في Docker بطيء ويستهلك الذاكرة، لذلك الشائع على سيرفر Linux هو network_mode: host، وراجع شبكات Docker وأفضل الممارسات قبل أن تقرر.
  • فحص الصحة Health Check يسأل Asterisk نفسه عن مدة تشغيله، فإذا توقف عن الرد يظهر ال Container بحالة unhealthy.

والملف conf/asterisk.conf يحدد المجلدات، والسطر المهم فيه هو astdbdir:

[directories]
astcachedir => /var/cache/asterisk
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
; AstDB (registrations, call forward, DND...) lives on the spool volume so it survives a new container
astdbdir => /var/spool/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/spool/asterisk
astrundir => /var/run/asterisk
astlogdir => /var/log/asterisk
astsbindir => /usr/sbin

[options]
documentation_language = en_US

AstDB قاعدة SQLite صغيرة يحفظ فيها Asterisk ما يتغير أثناء التشغيل، ومنها عناوين الهواتف المسجلة. وفي البداية تركناها في مكانها الافتراضي /var/lib/asterisk داخل ال Container، ثم أعدنا إنشاء ال Container بالأمر docker compose up -d بعد تعديل بسيط، فاتصلت Alice ب Bob وكانت النتيجة كما يلي:

[2026-10-20 08:38:05] ERROR[36]: res_pjsip.c:991 ast_sip_create_dialog_uac: Endpoint '1002': Could not create dialog to invalid URI '1002'.  Is endpoint registered and reachable?
[2026-10-20 08:38:05] NOTICE[125][C-00000002]: app_dial.c:2739 dial_exec_full: Unable to create channel of type 'PJSIP' (cause 3 - No route to destination)
    -- Executing [1002@office:4] NoOp("PJSIP/1001-00000002", "DIALSTATUS=CHANUNAVAIL") in new stack

والسبب أن ال Container الجديد بدأ بقاعدة فارغة فنسي أن Bob مسجل، وهاتف Bob لن يسجل من جديد قبل انتهاء مدة تسجيله، وهي نحو ساعة، فكل مكالمة إليه خلال هذه الساعة تفشل. وعندما نقلنا القاعدة إلى ال Volume بقي تسجيل Bob بعد إعادة إنشاء ال Container. ولاحظ أن الملف النموذجي يكتب القسم [directories](!)، وقد حذفنا (!) لأنها تجعل القسم قالباً Template، وفي تجربتنا تجاهل Asterisk القيمة الجديدة لـ astdbdir وبقي على المسار القديم حتى حذفناها، ويمكنك أن تتأكد من المسار بالأمر:

docker compose up -d
docker exec arb-ast-pbx asterisk -rx 'core show settings' | grep ASTDB
docker compose ps --format '{{.Name}} {{.Status}}'

والمخرج سوف يكون كما يلي:

  ASTDB:                       /var/spool/asterisk/astdb
arb-ast-pbx Up 41 seconds (healthy)

التحويلات في PJSIP بالقوالب

شرحنا في مقال تتبع المكالمة أن كل تحويلة في PJSIP ثلاثة أجزاء بالاسم نفسه: endpoint وauth وaor، والعلاقة بينها مشروحة في وثائق PJSIP. والمشكلة أن كل تحويلة جديدة تعني ثلاثة أقسام، ومع عشرين موظفاً يصبح الملف طويلاً وسهل الخطأ، لذلك سوف نضع لكل جزء قالباً Template ترث منه كل التحويلات. وهذا هو الملف conf/pjsip.conf:

[global]
type=global
user_agent=ArabRoot-PBX

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

; Templates: every extension inherits from these three
[endpoint-office](!)
type=endpoint
context=office
disallow=all
allow=ulaw,alaw
direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes
dtmf_mode=rfc4733

[auth-office](!)
type=auth
auth_type=userpass

[aor-office](!)
type=aor
max_contacts=1
remove_existing=yes
qualify_frequency=30

; Extension 1001
[1001](endpoint-office)
auth=1001
aors=1001
callerid="Alice" <1001>
mailboxes=1001@office

[1001](auth-office)
username=1001
password=Vq7-alice-9f3K2m

[1001](aor-office)

; Extension 1002
[1002](endpoint-office)
auth=1002
aors=1002
callerid="Bob" <1002>
mailboxes=1002@office

[1002](auth-office)
username=1002
password=Zr4-bob-6pW8xd

[1002](aor-office)

في الإعداد أعلاه لاحظ التالي:

  • القوالب الثلاثة تنتهي ب (!)، والتحويلة ترث منها بكتابة اسم القالب بين قوسين بعد اسمها، فإضافة موظف جديد تعني ثلاثة أقسام قصيرة فيها الاسم وكلمة المرور فقط.
  • context=office هو أهم سطر في الملف، فهو يحدد في أي قسم من خطة الاتصال تبدأ مكالمات هذه التحويلة، وبالتالي ما الأرقام التي يحق لها الاتصال بها.
  • خيارات NAT الثلاثة rtp_symmetric وforce_rport وrewrite_contact مع qualify_frequency هي نتيجة مقال تتبع المكالمة، فلن نعيد شرحها.
  • dtmf_mode=rfc4733 يرسل أرقام لوحة المفاتيح DTMF كحزم RTP خاصة، وهي الطريقة التي يفهمها أغلب الهواتف، وسوف نحتاجها في الرد الآلي.
  • mailboxes يربط التحويلة بصندوق بريدها الصوتي، فيرسل المقسم للهاتف إشعاراً عندما تصل رسالة جديدة، وهو ما يضيء لمبة الرسائل في الهاتف المكتبي.

وإذا وجدت القوالب ما زالت طويلة، فهناك طريقة أقصر اسمها PJSIP Configuration Wizard تكتب التحويلة في قسم واحد، ولكننا اخترنا القوالب لأن ما يظهر في أوامر التشخيص يطابق ما في الملف تماماً.

بعد تشغيل المقسم نسأله عن التحويلات:

docker exec arb-ast-pbx asterisk -rx 'pjsip show endpoints'

والمخرج سوف يكون كما يلي، بعد حذف ترويسة الجدول:

 Endpoint:  1001/1001                                            Unavailable   0 of inf
     InAuth:  1001/1001
        Aor:  1001                                               1

 Endpoint:  1002/1002                                            Unavailable   0 of inf
     InAuth:  1002/1002
        Aor:  1002                                               1


Objects found: 2

التحويلتان Unavailable لأن أي هاتف لم يسجل بعد. الآن نشغل هاتف Bob، وهو baresip في Container كما في مقال تتبع المكالمة، مع ملف accounts فيه اسم المستخدم وكلمة المرور:

echo "<sip:[email protected]>;auth_pass=Zr4-bob-6pW8xd;answermode=auto" > bob/accounts
docker run -d --name arb-ast-bob --network arb-ast-net --ip 10.88.0.22 \
  -v "$PWD/bob:/conf" -v "$PWD/sounds:/sounds:ro" -v "$PWD/rec-bob:/rec" \
  arb-ast-phone:1 baresip -f /conf
docker exec arb-ast-pbx asterisk -rx 'pjsip show contacts'

و arb-ast-phone:1 Image صغيرة بنيناها من debian:12.15 فيها baresip وsox وcurl وnc وPython لتجربة الهواتف والبرامج، وملف bob/config هو نفسه في مقال تتبع المكالمة. والمخرج سوف يكون كما يلي:

  Contact:  1002/sip:[email protected]:5060   98d49d3a4b Avail         3.075

Objects found: 1

التحويلة 1002 مسجلة وحالتها Avail مع زمن الرد بالملي ثانية. أما هاتف Alice فنشغله عند كل مكالمة بسكربت صغير يرسل له أمر /dial ثم الأرقام ثم /hangup، ولهذا تظهر 1001 غير مسجلة بين المكالمات.

أساسيات خطة الاتصال Dialplan

خطة الاتصال في extensions.conf مقسمة إلى سياقات Contexts، وكل سياق فيه امتدادات Extensions، وكل امتداد فيه خطوات مرقمة اسمها الأولويات Priorities، والقاعدة كما في وثائق خطة الاتصال أن Asterisk يبحث في سياق القناة عن امتداد يطابق الرقم المطلوب، ثم ينفذ أولوياته بالترتيب من 1. ولنأخذ أبسط سطر:

[office]
exten => _10XX,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN})
 same => n,Dial(PJSIP/${EXTEN},15)
  • [office] هو السياق الذي حددناه في القالب endpoint-office.
  • _10XX نمط Pattern لأنه يبدأ ب _، وX تعني أي رقم من 0 إلى 9، فالنمط يطابق من 1000 إلى 1099، والأحرف الأخرى مشروحة في وثائق الأنماط.
  • same => n تعني الامتداد نفسه والأولوية التالية، فلا تحتاج أن ترقم الأسطر بنفسك.
  • ${EXTEN} متغير فيه الرقم المطلوب، و${CALLERID(num)} دالة تعيد رقم المتصل.
  • Dial(PJSIP/${EXTEN},15) يرن على التحويلة 15 ثانية، وعندما ينتهي يضع النتيجة في المتغير DIALSTATUS كما في وثائق Dial، مثل ANSWER وNOANSWER وBUSY وCHANUNAVAIL.

والصورة التالية تبين ما سوف نبنيه في هذا القسم: تسجيل المكالمة، ثم الرنين، ثم البريد الصوتي بحسب سبب عدم الرد:

Alice تتصل بالرقم 1002، فيدخل الاتصال السياق office بالنمط _10XX، الأولوية 1 و2 للمتغيرات، ثم MixMonitor يبدأ التسجيل، ثم Dial يرن 15 ثانية، ثم DIALSTATUS يقرر: ANSWER يربط المكالمة، وNOANSWER أو CHANUNAVAIL يذهب إلى VoiceMail برسالة غير متاح، وBUSY إلى VoiceMail برسالة مشغول، وتحتها مسارات ملفات التسجيل والبريد الصوتي على القرص
كل سطر أولوية تنفذ بالترتيب، وDIALSTATUS يحدد الطريق بعد الرنين

تسجيل المكالمات Call Recording

التطبيق المسؤول عن التسجيل هو MixMonitor، وهو يسجل صوت الطرفين في ملف واحد في /var/spool/asterisk/monitor. والحل البسيط الذي يكتبه أغلب الناس أول مرة هو تشغيله قبل Dial مباشرة:

exten => _10XX,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN})
 same => n,Set(REC=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,MixMonitor(${REC}.wav)
 same => n,Dial(PJSIP/${EXTEN},15)

اتصلت Alice ب Bob، وهذا ما ظهر في سجل المقسم:

    -- Executing [1002@office:1] NoOp("PJSIP/1001-00000000", "Call from 1001 to 1002") in new stack
    -- Executing [1002@office:2] Set("PJSIP/1001-00000000", "REC=20261020-082152-1001-1002") in new stack
    -- Executing [1002@office:3] MixMonitor("PJSIP/1001-00000000", "20261020-082152-1001-1002.wav") in new stack
    -- Executing [1002@office:4] Dial("PJSIP/1001-00000000", "PJSIP/1002,15") in new stack
  == Begin MixMonitor Recording PJSIP/1001-00000000
    -- Called PJSIP/1002
    -- PJSIP/1002-00000001 is ringing
    -- PJSIP/1002-00000001 answered PJSIP/1001-00000000
    -- Channel PJSIP/1002-00000001 joined 'simple_bridge' basic-bridge <0801aaaf-9af8-44df-b7ce-c0b6ef682067>
    -- Channel PJSIP/1001-00000000 joined 'simple_bridge' basic-bridge <0801aaaf-9af8-44df-b7ce-c0b6ef682067>
    -- Channel PJSIP/1001-00000000 left 'simple_bridge' basic-bridge <0801aaaf-9af8-44df-b7ce-c0b6ef682067>
    -- Channel PJSIP/1002-00000001 left 'simple_bridge' basic-bridge <0801aaaf-9af8-44df-b7ce-c0b6ef682067>
  == Spawn extension (office, 1002, 4) exited non-zero on 'PJSIP/1001-00000000'
  == MixMonitor close filestream (mixed)
  == End MixMonitor Recording PJSIP/1001-00000000

ولاحظ هنا كيف يظهر ما رسمناه: قناتان، وجسر يدخلانه ويخرجان منه. وعند فحص الملف الناتج بالأداة soxi كان المخرج كما يلي:

Channels       : 1
Sample Rate    : 8000
Precision      : 16-bit
Duration       : 00:00:08.08 = 64640 samples ~ 606 CDDA sectors
File Size      : 129k

أي 16 KB لكل ثانية، و57.6 MB لكل ساعة تسجيل، فإذا كان في مكتبك 20 موظفاً يتكلم كل منهم ساعتين في اليوم فهذا نحو 2.3 GB يومياً، لذلك خطط للمساحة وللمدة التي تحتفظ فيها بالتسجيلات قبل أن تشغل التسجيل لكل المكالمات.

ولكن هذا الحل له مشكلتان ظهرتا في التجربة نفسها: الأولى أن التسجيل يبدأ قبل الرد، فإذا لم يرد Bob سجل الملف الرنين ورسالة البريد الصوتي كاملة، والثانية أنه يسجل حتى المكالمات الفاشلة، فعندما اتصلت Alice بالرقم 1003، وهو يطابق النمط ولا توجد تحويلة بهذا الرقم، نفذ المقسم MixMonitor وأنشأ ملفاً لمكالمة لم تحدث. والحل الأنسب هو أن يبدأ التسجيل لحظة الرد فقط، وهذا ما يفعله الخيار U في Dial، فهو يشغل روتيناً فرعياً Subroutine على قناة الطرف الذي رد، بعد الرد وقبل ربط الطرفين:

exten => _10XX,1,NoOp(Call from ${CALLERID(num)} to ${EXTEN})
 same => n,Set(REC=${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${CALLERID(num)}-${EXTEN})
 same => n,Dial(PJSIP/${EXTEN},15,U(record-on-answer^${REC}))

; Runs on the answering channel right after answer: record only the conversation
[record-on-answer]
exten => s,1,MixMonitor(${ARG1}.wav)
 same => n,Return()

ثم حذفنا التسجيلات القديمة، واتصلت Alice ب Bob فرد، ثم اتصلت بالرقم 1003:

docker exec arb-ast-pbx asterisk -rx 'dialplan reload'
docker exec arb-ast-pbx ls -l /var/spool/asterisk/monitor/

والمخرج سوف يكون كما يلي:

Dialplan reloaded.
total 128
-rw-r--r-- 1 asterisk asterisk 127084 Oct 20 08:32 20261020-083243-1001-1002.wav

ملف واحد للمكالمة التي تمت فقط. ويجب أن تعرف أن تسجيل المكالمات له جانب قانوني قبل الجانب التقني، فراجع الأنظمة التي تنطبق على مؤسستك، والممارسة الشائعة أن يسمع المتصل رسالة تخبره أن المكالمة قد تسجل، ويمكنك إضافتها بالملف for-quality-purposes الموجود في ملفات الصوت.

البريد الصوتي Voicemail

البريد الصوتي يعرف في conf/voicemail.conf، وكل سطر صندوق بريد فيه رقم سري يدخله الموظف من هاتفه، والاسم، والبريد الإلكتروني الذي تصله الرسالة عليه:

[general]
format=wav
maxmsg=100
maxsecs=120
minsecs=2
maxlogins=3
attach=yes
[email protected]

[office]
1001 => 4826,Alice,[email protected]
1002 => 7391,Bob,[email protected]

ثم نكمل الامتداد بعد Dial، فإذا كان الخط مشغولاً يسمع المتصل رسالة «مشغول»، وفي غير ذلك رسالة «غير متاح»، ونضيف الرقم *97 ليستمع الموظف إلى بريده:

 same => n,NoOp(DIALSTATUS=${DIALSTATUS})
 same => n,GotoIf($["${DIALSTATUS}" = "BUSY"]?busy:unavail)
 same => n(unavail),VoiceMail(${EXTEN}@office,u)
 same => n,Hangup()
 same => n(busy),VoiceMail(${EXTEN}@office,b)
 same => n,Hangup()

; Listen to your own voicemail
exten => *97,1,VoiceMailMain(${CALLERID(num)}@office)
 same => n,Hangup()

في الإعداد أعلاه لاحظ التالي:

  • GotoIf يقفز إلى العنوان busy أو unavail بحسب الشرط، والعناوين Labels هي الأسماء بين القوسين بعد n.
  • الخياران u وb في VoiceMail يختاران رسالة الترحيب، وVoiceMailMain هو قائمة الاستماع إلى الرسائل.

جعلنا هاتف Bob لا يرد تلقائياً، واتصلت Alice وأغلقت بعد 30 ثانية، فكان المخرج كما يلي:

    -- Executing [1002@office:5] NoOp("PJSIP/1001-00000002", "DIALSTATUS=NOANSWER") in new stack
    -- Executing [1002@office:6] GotoIf("PJSIP/1001-00000002", "0?busy:unavail") in new stack
    -- Executing [1002@office:7] VoiceMail("PJSIP/1001-00000002", "1002@office,u") in new stack
    -- Recording the message
    -- Recording was 1 seconds long but needs to be at least 2 - abandoning

لم تحفظ أي رسالة. والسبب أن المتصل بعد 15 ثانية من الرنين يسمع رسالة الترحيب كاملة قبل الصافرة، وقد أخذت هنا نحو 13 ثانية، فلم يبق للكلام إلا ثانية واحدة، وهي أقل من minsecs=2، فرفضها Asterisk. وهذا مهم عندما تختار مدة الرنين، فالمتصل الذي ينتظر 30 ثانية ثم يسمع رسالة طويلة يغلق غالباً قبل الصافرة. أعدنا المكالمة لمدة 40 ثانية، ثم نظرنا في صندوق Bob:

docker exec arb-ast-pbx ls -l /var/spool/asterisk/voicemail/office/1002/INBOX/
docker exec arb-ast-pbx asterisk -rx 'voicemail show users'

والمخرج سوف يكون كما يلي:

-rw-rw-rw- 1 asterisk asterisk    280 Oct 20 08:24 msg0000.txt
-rw-r--r-- 1 asterisk asterisk 179884 Oct 20 08:24 msg0000.wav
Context    Mbox  User                      Zone       NewMsg
office     1001  Alice                                     0
office     1002  Bob                                       1
2 voicemail users configured.

والملف msg0000.txt يحفظ معلومات الرسالة، ومنها callerid="Alice" <1001> وduration=11. أما إرسال الرسالة بالبريد الإلكتروني فقد فشل، وهذا السطر من سجل المقسم يبين السبب:

/bin/sh: 1: /usr/sbin/sendmail: not found

Asterisk لا يرسل البريد بنفسه، وإنما يستدعي البرنامج /usr/sbin/sendmail، فإذا أردت إرسال الرسائل فأضف إلى ال Image برنامجاً مثل msmtp يرسل عبر سيرفر بريد موجود، وقد شرحنا الفرق بين الإرسال المباشر والترحيل في الفرق بين SMTP والترحيل Relay.

بعد ذلك اتصلت Alice ب *97 وأدخلت رقمها السري 4826#:

    -- Executing [*97@office:1] VoiceMailMain("PJSIP/1001-0000000d", "1001@office") in new stack
    -- <PJSIP/1001-0000000d> Playing 'vm-password.ulaw' (language 'en')
    -- <PJSIP/1001-0000000d> Playing 'vm-youhave.ulaw' (language 'en')
    -- <PJSIP/1001-0000000d> Playing 'digits/1.ulaw' (language 'en')
    -- <PJSIP/1001-0000000d> Playing 'vm-INBOX.ulaw' (language 'en')
    -- <PJSIP/1001-0000000d> Playing 'vm-message.ulaw' (language 'en')

ومع رقم سري خاطئ يسمع المتصل vm-incorrect ثم يطلب منه الرقم مرة أخرى، وبعد maxlogins=3 محاولات يغلق المقسم الخط. ولاحظ أن الرقم السري المكون من أربعة أرقام ضعيف، وصندوق البريد الذي يمكن الوصول إليه من خارج المكتب كان باباً معروفاً للاحتيال، لذلك لا تقم بالسماح ل VoiceMailMain من الخطوط الخارجية إلا بحاجة واضحة.

الرد الآلي IVR

الرد الآلي Interactive Voice Response، واختصاراً IVR، هو القائمة التي يسمعها المتصل بالرقم الرئيسي: «للمبيعات اضغط 1، وللدعم الفني اضغط 2». والصورة التالية تبين القائمة التي سوف نبنيها:

متصل يطلب الرقم 500، فيتحقق GotoIfTime من وقت الدوام من الأحد إلى الخميس، فإذا كان خارج الدوام يسمع رسالة الإغلاق ثم يذهب إلى البريد الصوتي 1001، وإذا كان داخل الدوام يسمع القائمة عبر BackGround ثم ينتظر WaitExten خمس ثوان، والرقم 1 يذهب إلى 1001 للمبيعات و2 إلى 1002 للدعم و0 إلى 1001 للموظف، والامتداد i عند الاختيار غير الموجود وt عند انتهاء الوقت يعيدان القائمة ما دام عدد المحاولات أقل من ثلاث
وقت الدوام أولاً، ثم القائمة، ثم الاختيار أو إعادة المحاولة

وهذا هو الإعداد كاملاً، أضفناه إلى extensions.conf:

[globals]
OFFICE_HOURS=08:00-16:59
OFFICE_DAYS=sun-thu
OFFICE_TZ=Asia/Riyadh

[office]
; The main menu (IVR)
exten => 500,1,Goto(ivr-main,s,1)

[ivr-main]
exten => s,1,Answer()
 same => n,Set(TRIES=0)
 same => n,GotoIfTime(${OFFICE_HOURS},${OFFICE_DAYS},*,*,${OFFICE_TZ}?menu)
 same => n,Playback(closed)
 same => n,VoiceMail(1001@office,u)
 same => n,Hangup()
 same => n(menu),Set(TRIES=$[${TRIES} + 1])
 same => n,Background(custom/welcome-ar&for-sales&press-1&for-tech-support&press-2&to-reach-operator&press-0)
 same => n,WaitExten(5)

exten => 1,1,NoOp(IVR choice 1: sales)
 same => n,Goto(office,1001,1)
exten => 2,1,NoOp(IVR choice 2: support)
 same => n,Goto(office,1002,1)
exten => 0,1,Goto(office,1001,1)

exten => i,1,Playback(option-is-invalid)
 same => n,GotoIf($[${TRIES} >= 3]?office,1001,1:s,menu)
exten => t,1,GotoIf($[${TRIES} >= 3]?office,1001,1:s,menu)

في الإعداد أعلاه لاحظ التالي:

  • BackGround يشغل الرسالة ويستمع للأرقام في الوقت نفسه، فالمتصل الذي يعرف الرقم يضغط دون أن ينتظر نهاية القائمة، أما Playback فيجبره على الانتظار، لذلك استخدم Playback للرسائل التي يجب أن تسمع كاملة فقط.
  • WaitExten ينتظر خمس ثوان بعد انتهاء القائمة، والامتدادان i وt اسمان خاصان يقفز إليهما Asterisk عند ضغط رقم لا يوجد له امتداد، أو عند انتهاء الوقت دون ضغط شيء.
  • العداد TRIES يمنع المتصل من الدوران في القائمة إلى الأبد، فبعد المحاولة الثالثة يحول إلى الموظف، والمتصل الذي لا يجيد استخدام القائمة يجد إنساناً يرد عليه.
  • GotoIfTime يأخذ الوقت ثم الأيام ثم أيام الشهر ثم الأشهر ثم المنطقة الزمنية، ووضعنا القيم في [globals] حتى تغيرها في مكان واحد.

والمنطقة الزمنية في الإعداد ليست زينة، فال Container يعمل بتوقيت UTC، كما يظهر من الأمر docker exec arb-ast-pbx date الذي أعاد Tue Oct 20 08:26:12 UTC 2026، وبدون Asia/Riyadh سوف يبدأ دوامك في الرد الآلي عند الحادية عشرة صباحاً بتوقيت الرياض وينتهي في الثامنة مساءً. اتصلت Alice بالرقم 500 وضغطت 2 أثناء القائمة:

    -- Executing [s@ivr-main:3] GotoIfTime("PJSIP/1001-00000006", "08:00-16:59,sun-thu,*,*?menu") in new stack
    -- Executing [s@ivr-main:7] Set("PJSIP/1001-00000006", "TRIES=1") in new stack
    -- <PJSIP/1001-00000006> Playing 'for-sales.ulaw' (language 'en')
    -- <PJSIP/1001-00000006> Playing 'press-1.ulaw' (language 'en')
    -- <PJSIP/1001-00000006> Playing 'for-tech-support.ulaw' (language 'en')
    -- <PJSIP/1001-00000006> Playing 'press-2.ulaw' (language 'en')
    -- <PJSIP/1001-00000006> Playing 'to-reach-operator.ulaw' (language 'en')
    -- Executing [2@ivr-main:1] NoOp("PJSIP/1001-00000006", "IVR choice 2: support") in new stack
    -- Executing [2@ivr-main:2] Goto("PJSIP/1001-00000006", "office,1002,1") in new stack
    -- Executing [1002@office:4] Dial("PJSIP/1001-00000006", "PJSIP/1002,15") in new stack
    -- PJSIP/1002-00000007 answered PJSIP/1001-00000006

ولاحظ أن الرقم 2 قطع الرسالة to-reach-operator قبل نهايتها، وهذا هو الفرق بين BackGround وPlayback. وهذه المحاولة كانت قبل أن نضيف المنطقة الزمنية، ثم كررنا المكالمة وضغطت Alice الرقم 7 ثم لم تضغط شيئاً:

    -- Executing [s@ivr-main:7] Set("PJSIP/1001-00000008", "TRIES=1") in new stack
    -- Executing [i@ivr-main:1] Playback("PJSIP/1001-00000008", "option-is-invalid") in new stack
    -- Executing [i@ivr-main:2] GotoIf("PJSIP/1001-00000008", "0?office,1001,1:s,menu") in new stack
    -- Executing [s@ivr-main:7] Set("PJSIP/1001-00000008", "TRIES=2") in new stack
    -- Executing [s@ivr-main:9] WaitExten("PJSIP/1001-00000008", "5") in new stack
    -- Executing [t@ivr-main:1] GotoIf("PJSIP/1001-00000008", "0?office,1001,1:s,menu") in new stack
    -- Executing [s@ivr-main:7] Set("PJSIP/1001-00000008", "TRIES=3") in new stack
    -- Executing [s@ivr-main:9] WaitExten("PJSIP/1001-00000008", "5") in new stack
    -- Executing [t@ivr-main:1] GotoIf("PJSIP/1001-00000008", "1?office,1001,1:s,menu") in new stack
    -- Executing [1001@office:1] NoOp("PJSIP/1001-00000008", "Call from 1001 to 1001") in new stack

الاختيار الخاطئ ذهب إلى i، ثم انتهى الوقت مرتين فذهب إلى t، وفي المحاولة الثالثة تحقق الشرط فتحولت المكالمة إلى 1001.

وقد يتساءل البعض: كيف أختبر فرع «خارج الدوام» دون أن أنتظر إلى المساء؟ والإجابة أن المتغيرات في [globals] يمكن تغييرها من سطر الأوامر دون تعديل الملف، ثم تعود إلى قيمتها عند إعادة التحميل:

docker exec arb-ast-pbx asterisk -rx 'dialplan set global OFFICE_HOURS 00:00-00:01'
docker exec arb-ast-pbx asterisk -rx 'dialplan reload'

وبين الأمرين اتصلت Alice بالرقم 500، فكان المخرج كما يلي:

    -- Global variable 'OFFICE_HOURS' set to '00:00-00:01'
    -- Executing [s@ivr-main:3] GotoIfTime("PJSIP/1001-0000000a", "00:00-00:01,sun-thu,*,*,Asia/Riyadh?menu") in new stack
    -- Executing [s@ivr-main:4] Playback("PJSIP/1001-0000000a", "closed") in new stack
    -- <PJSIP/1001-0000000a> Playing 'closed.ulaw' (language 'en')
    -- Executing [s@ivr-main:5] VoiceMail("PJSIP/1001-0000000a", "1001@office,u") in new stack

رسائلك الصوتية بالعربية

ملفات الصوت التي تأتي مع Asterisk بالإنجليزية، ولذلك في الواقع سوف تسجل رسائلك بنفسك، والشرط الوحيد أن يكون الملف بصيغة يقرؤها Asterisk دون تحويل في كل مكالمة، والأبسط WAV بتردد 8000 Hz وقناة واحدة. سجل الرسالة بأي برنامج، ثم حولها بالأداة sox:

mkdir -p prompts
sox raw-recording.wav -r 8000 -c 1 -b 16 prompts/welcome-ar.wav
soxi prompts/welcome-ar.wav | grep -E "Channels|Sample Rate|Duration"

والمخرج سوف يكون كما يلي:

Channels       : 1
Sample Rate    : 8000
Duration       : 00:00:03.00 = 24000 samples ~ 225 CDDA sectors

والمجلد prompts مربوط في ملف Compose على /var/lib/asterisk/sounds/custom، لذلك تكتب في خطة الاتصال custom/welcome-ar دون الامتداد، كما في القائمة أعلاه، وعند الاتصال يظهر في السجل:

    -- <PJSIP/1001-00000000> Playing 'custom/welcome-ar.slin' (language 'en')

AMI: برامجك تراقب المكالمات وتبدأها

واجهة Asterisk Manager Interface، واختصاراً AMI، اتصال TCP على المنفذ 5038، يرسل منه Asterisk الأحداث Events لحظة حدوثها، مثل قناة جديدة أو رنين أو رد، ويستقبل منه الأوامر Actions مثل بدء مكالمة. وهذه هي الطريقة التقليدية التي تربط بها لوحات المراقبة وأنظمة CRM بالمقسم، كأن تظهر بطاقة العميل لموظف الدعم عندما يرن هاتفه. والإعداد في conf/manager.conf:

[general]
enabled=yes
port=5038
bindaddr=0.0.0.0

[crm]
secret=Hm2-ami-8tR5qL
deny=0.0.0.0/0.0.0.0
permit=10.88.0.0/255.255.255.0
read=call,reporting
write=originate

في الإعداد أعلاه لاحظ التالي:

  • deny ثم permit يسمحان بالدخول من شبكة التجربة فقط، فحتى لو تسرب ال secret لا يقبله المقسم من عنوان آخر.
  • read وwrite صلاحيات بالفئات Classes، وقد أعطينا المستخدم crm قراءة أحداث المكالمات وبدء المكالمات فقط، وليس صلاحية system التي تسمح بإيقاف المقسم أو command التي تنفذ أي أمر CLI.
  • AMI لا يشفر الاتصال بشكل افتراضي، وال secret يمر نصاً واضحاً، لذلك لا تقم بفتح المنفذ 5038 على الإنترنت أبداً، واجعل البرنامج الذي يستخدمه على السيرفر نفسه أو في شبكة خاصة.

البروتوكول نصي بسيط كما في وثائق بروتوكول AMI: أسطر Key: Value تنتهي بسطر فارغ، لذلك يكفي nc لتجربته. السكربت ami-test.sh التالي يسجل الدخول، ثم يطلب قائمة التحويلات، ثم يبدأ مكالمة:

cat > ami-test.sh <<'EOF'
#!/bin/sh
# Logs in to AMI, asks for the endpoint list, starts a call, and prints what comes back for 12 seconds
{
  printf 'Action: Login\r\nUsername: crm\r\nSecret: Hm2-ami-8tR5qL\r\n\r\n'
  sleep 1
  printf 'Action: PJSIPShowEndpoints\r\nActionID: list-1\r\n\r\n'
  sleep 1
  printf 'Action: Originate\r\nActionID: call-1\r\nChannel: PJSIP/1002\r\nContext: office\r\nExten: 500\r\nPriority: 1\r\nCallerID: "CRM" <1002>\r\nAsync: true\r\n\r\n'
  sleep 12
  printf 'Action: Logoff\r\n\r\n'
} | nc 10.88.0.10 5038
EOF
docker run --rm --network arb-ast-net -v "$PWD/ami-test.sh:/ami-test.sh:ro" arb-ast-phone:1 sh /ami-test.sh | tr -d '\r'

والمخرج سوف يكون كما يلي، بعد أن اختصرناه إلى البداية وبعض الأحداث:

Asterisk Call Manager/11.0.0
Response: Success
Message: Authentication accepted

Response: Error
ActionID: list-1
Message: Permission denied

Response: Success
ActionID: call-1
Message: Originate successfully queued

Event: DialEnd
Privilege: call,all
DestChannel: PJSIP/1002-0000000f
DestChannelState: 6
DestChannelStateDesc: Up
DestCallerIDNum: 1002
DestCallerIDName: CRM
DestContext: office
DestExten: s
DestUniqueid: 1792485002.15
DialStatus: ANSWER

Event: OriginateResponse
Privilege: call,all
ActionID: call-1
Response: Success
Channel: PJSIP/1002-0000000f
Context: office
Exten: 500
Reason: 4
Uniqueid: 1792485002.15

في المخرج أعلاه لاحظ التالي:

  • طلب قائمة التحويلات رفض ب Permission denied، لأنه يحتاج صلاحية لم نعطها لهذا المستخدم، وهذا بالضبط ما نريده: كل برنامج يأخذ ما يحتاجه فقط، وإذا احتجت القائمة فأضف الفئة المطلوبة لهذا المستخدم وحده.
  • Originate رن هاتف Bob أولاً، وعندما رد نقل المكالمة إلى الامتداد 500 في السياق office. وهذه هي فكرة «الاتصال بضغطة زر» Click to Call في أنظمة CRM: يرن هاتف الموظف، ثم يتصل المقسم بالرقم المطلوب.
  • كل حدث يحمل Uniqueid، وهو ما يربط به برنامجك الأحداث الكثيرة للمكالمة نفسها، وقد وصلت خلال 12 ثانية أحداث كثيرة منها تقارير RTCP، لذلك اختر الفئات التي تحتاجها، وإلا امتلأ برنامجك بأحداث لا يستخدمها.

ARI: برنامجك يتحكم بالمكالمة نفسها

AMI يراقب ويبدأ المكالمات، ولكن خطة الاتصال تبقى هي التي تقرر ما يحدث داخل المكالمة. أما Asterisk REST Interface، واختصاراً ARI، فيسلم المكالمة كاملة لبرنامجك: خطة الاتصال ترسلها إلى التطبيق Stasis، فيصل الحدث إلى برنامجك عبر WebSocket، ثم يرد برنامجك ويشغل الرسائل ويربط القنوات بطلبات HTTP عادية. وهذه هي الطريقة الحديثة لبناء أنظمة مثل الرد الآلي المرتبط بقاعدة بيانات أو المساعد الصوتي. ونحتاج تشغيل سيرفر HTTP المدمج وتعريف مستخدم:

; conf/http.conf
[general]
enabled=yes
bindaddr=0.0.0.0
bindport=8088

; conf/ari.conf
[general]
enabled=yes

[app]
type=user
read_only=no
password=Kc9-ari-3vN7pD

ثم نضيف إلى السياق office امتداداً يسلم المكالمة للتطبيق hello-ari:

; An ARI application takes the call
exten => 600,1,Stasis(hello-ari)
 same => n,Hangup()

وقبل كتابة أي برنامج، نجرب ARI ب curl من Container على الشبكة نفسها:

docker run --rm --network arb-ast-net arb-ast-phone:1 sh -c '
  curl -s -u app:Kc9-ari-3vN7pD http://10.88.0.10:8088/ari/endpoints; echo
  curl -s -o /dev/null -w "%{http_code}\n" -u app:wrong http://10.88.0.10:8088/ari/endpoints'

والمخرج سوف يكون كما يلي:

[{"technology":"PJSIP","resource":"1001","state":"offline","channel_ids":[]},{"technology":"PJSIP","resource":"1002","state":"online","channel_ids":[]}]
401

والآن البرنامج نفسه، وهو بلغة Python ولا يحتاج إلا المكتبة websockets: يتصل بالأحداث، وعندما تدخل مكالمة يرد عليها ويشغل رسالة، وعندما تنتهي الرسالة يغلق الخط:

# A minimal ARI application: answers every call that enters Stasis(hello-ari),
# plays one prompt, then hangs up. Uses only the Python standard library + websockets.
import asyncio, base64, json, urllib.request
import websockets

ARI = "http://10.88.0.10:8088/ari"
USER, PASSWORD, APP = "app", "Kc9-ari-3vN7pD", "hello-ari"
AUTH = "Basic " + base64.b64encode(f"{USER}:{PASSWORD}".encode()).decode()

def rest(method, path):
    req = urllib.request.Request(ARI + path, method=method, headers={"Authorization": AUTH})
    with urllib.request.urlopen(req) as r:
        body = r.read()
        print(f"  {method} {path} -> {r.status}")
        return json.loads(body) if body else None

async def main():
    url = f"ws://10.88.0.10:8088/ari/events?app={APP}&api_key={USER}:{PASSWORD}"
    async with websockets.connect(url) as ws:
        print("connected, waiting for calls")
        async for msg in ws:
            ev = json.loads(msg)
            kind = ev["type"]
            if kind == "StasisStart":
                ch = ev["channel"]
                print(f"{kind}: {ch['name']} from {ch['caller']['number']} to {ch['dialplan']['exten']}")
                rest("POST", f"/channels/{ch['id']}/answer")
                rest("POST", f"/channels/{ch['id']}/play?media=sound:tt-monkeys")
            elif kind == "PlaybackFinished":
                chan = ev["playback"]["target_uri"].split(":", 1)[1]
                print(f"{kind}: {ev['playback']['media_uri']}")
                rest("DELETE", f"/channels/{chan}")
            elif kind == "StasisEnd":
                print(f"{kind}: {ev['channel']['name']}")
                return

asyncio.run(main())

شغلنا البرنامج في Container على الشبكة نفسها، ثم اتصلت Alice بالرقم 600:

docker run -d --name arb-ast-ari --network arb-ast-net \
  -v "$PWD/ari-hello.py:/ari-hello.py:ro" arb-ast-phone:1 python3 -u /ari-hello.py
docker logs arb-ast-ari

والمخرج سوف يكون كما يلي:

connected, waiting for calls
StasisStart: PJSIP/1001-00000010 from 1001 to 600
  POST /channels/1792485046.16/answer -> 204
  POST /channels/1792485046.16/play?media=sound:tt-monkeys -> 201
PlaybackFinished: sound:tt-monkeys
  DELETE /channels/1792485046.16 -> 204
StasisEnd: PJSIP/1001-00000010

ولاحظ أن خطة الاتصال لم تفعل شيئاً إلا تسليم المكالمة، وكل ما بعد ذلك قرره البرنامج. ثم أوقفنا البرنامج واتصلنا بالرقم 600 مرة أخرى، وهذا ما يحدث عندما يتوقف برنامجك:

    -- Executing [600@office:1] Stasis("PJSIP/1001-00000011", "hello-ari") in new stack
[2026-10-20 08:31:26] WARNING[492][C-0000000d]: app_stasis.c:129 app_exec: PJSIP/1001-00000011: Stasis app 'hello-ari' doesn't exist
    -- Executing [600@office:2] Hangup("PJSIP/1001-00000011", "") in new stack

التطبيق غير موجود فأغلق المقسم الخط مباشرة، لذلك إذا بنيت شيئاً مهماً على ARI فاجعل في خطة الاتصال بعد Stasis مساراً احتياطياً، كتحويل المكالمة إلى موظف، بدل Hangup. وانتبه أيضاً إلى أن http.conf في تجربتنا يستمع على كل العناوين، والأمر http show status يبين أن السيرفر نفسه يقدم أيضاً /metrics و/phoneprov و/ws، لذلك اجعل bindaddr=127.0.0.1 في الإنتاج إذا كان برنامجك على السيرفر نفسه، أو ضعه خلف Reverse Proxy بشهادة TLS.

التشخيص من سطر الأوامر CLI

كل ما سبق شخصناه بسطر الأوامر، وهو أول مكان تنظر فيه عند أي مشكلة، وتفاصيله في وثائق CLI. ويمكنك الدخول إليه تفاعلياً بالأمر docker exec -it arb-ast-pbx asterisk -rvvv، أو تنفيذ أمر واحد ب asterisk -rx. وهذه أهم الأوامر:

السؤالالأمر
هل التحويلة مسجلة؟ ومن أي عنوان؟pjsip show contacts وpjsip show endpoint 1002
ما المكالمات القائمة الآن؟core show channels
هل يصل الصوت؟ وكم حزمة ضاعت؟pjsip show channelstats
ماذا فهم Asterisk من خطة الاتصال؟dialplan show 500@office
ما رسائل SIP الفعلية؟pjsip set logger on ثم off
هل طبق التعديل؟dialplan reload أو module reload res_pjsip.so ثم اقرأ السجل
ما الإصدار والمسارات؟core show version وcore show settings

والمثال التالي أثناء مكالمة بين Alice وBob:

docker exec arb-ast-pbx asterisk -rx 'core show channels'
docker exec arb-ast-pbx asterisk -rx 'pjsip show channelstats'

والمخرج سوف يكون كما يلي، بعد حذف بعض أسطر الترويسة:

Channel                                                          Location                         State   Application(Data)
PJSIP/1002-00000016                                              (None)                           Up      AppDial((Outgoing Line))
PJSIP/1001-00000015                                              1002@office:3                    Up      Dial(PJSIP/1002,15,U(record-on
2 active channels
1 active call
16 calls processed

 BridgeId ChannelId ........ UpTime.. Codec.   Count    Lost Pct  Jitter   Count    Lost Pct  Jitter RTT....
 4fae1145 1001-00000015      00:00:07 ulaw      316       0    0   0.002    319       0    0   0.004   0.000
 4fae1145 1002-00000016      00:00:07 ulaw      319       0    0   0.002    316       0    0   0.004   0.000

خطأ شائع: reload ينجح والتعديل لم يطبق

لنكسر الإعداد عمداً ونرى ما يحدث، فنكتب في القالب rtp_symetric=yes بحرف m واحد، ثم نعيد تحميل PJSIP:

docker exec arb-ast-pbx asterisk -rx 'module reload res_pjsip.so'
docker logs arb-ast-pbx --since 5s
docker exec arb-ast-pbx asterisk -rx 'pjsip show endpoints'

والمخرج سوف يكون كما يلي، بعد الاختصار:

Module 'res_pjsip.so' reloaded successfully.
[2026-10-20 08:34:03] ERROR[35]: config_options.c:780 aco_process_var: Could not find option suitable for category '1001' named 'rtp_symetric' at line 17 of
[2026-10-20 08:34:03] ERROR[35]: res_sorcery_config.c:422 sorcery_config_internal_load: Could not create an object of type 'endpoint' with id '1001' from configuration file 'pjsip.conf'
[2026-10-20 08:34:03] ERROR[35]: config_options.c:780 aco_process_var: Could not find option suitable for category '1002' named 'rtp_symetric' at line 17 of
[2026-10-20 08:34:03] ERROR[35]: res_sorcery_config.c:422 sorcery_config_internal_load: Could not create an object of type 'endpoint' with id '1002' from configuration file 'pjsip.conf'

Objects found: 2

الأمر يقول «reloaded successfully»، والتحويلتان ما زالتا موجودتين لأن Asterisk أبقى النسخة القديمة منهما عندما فشل في إنشاء الجديدة، فإذا لم تقرأ السجل فسوف تظن أن تعديلك طبق. والأخطر ما يحدث عند إعادة تشغيل ال Container والخطأ ما زال في الملف:

docker restart arb-ast-pbx
docker exec arb-ast-pbx asterisk -rx 'pjsip show endpoints'
No objects found.

كل التحويلات اختفت، لأن الخطأ في القالب المشترك، وهذا عطل كامل للمكتب بسبب حرف واحد، ولن تراه إلا بعد أول إعادة تشغيل، ربما بعد أسابيع من التعديل. لذلك اقرأ السجل بعد كل reload، واحفظ ملفات الإعداد في Git حتى تعرف ماذا تغير ومتى.

رسائل تراها كثيراً

  • رقم لا يطابق أي امتداد: Call (UDP:10.88.0.21:5060) to extension '2000' rejected because extension not found in context 'office'، والسبب إما خطأ في الرقم أو أن التحويلة في سياق آخر.
  • رقم يطابق النمط ولا توجد تحويلة به: Unable to create PJSIP channel - endpoint '1003' was not found ثم No entry in voicemail config file for '1003'، فالنمط _10XX أوسع من تحويلاتك، وفي الإنتاج اجعل الأنماط على قدر الأرقام الموجودة.
  • كلمة مرور خاطئة من هاتف: Request 'REGISTER' from '<sip:[email protected]>' failed for '10.88.0.23:5060' (callid: 52bb871e25547b5b) - Failed to authenticate، وهذا السطر في الملف messages.log هو ما تقرؤه أدوات مثل fail2ban لحظر من يجرب كلمات المرور، وسوف نبنيه في مقال تأمين المقسم.
  • عند بدء التشغيل ترى أخطاء مثل cdr_custom declined to load، وهي من وحدات لا يوجد لها إعداد في الملفات النموذجية، ولا تؤثر على ما بنيناه.

التحقق من النجاح

  • docker compose ps يظهر ال Container بحالة healthy.
  • pjsip show contacts يظهر كل هاتف بحالة Avail.
  • مكالمة بين تحويلتين يسمع فيها الطرفان، وpjsip show channelstats بلا فقد يذكر.
  • مكالمة دون رد تنتهي برسالة في voicemail show users، ومكالمة مع رد تنتج ملفاً واحداً في /var/spool/asterisk/monitor.
  • الرقم 500 يشغل القائمة، والأرقام 1 و2 و0 تحول إلى التحويلات الصحيحة.
  • بعد docker compose up -d --force-recreate تبقى الهواتف مسجلة والرسائل موجودة.

النسخ الاحتياطي والاستعادة

في هذا الإعداد هناك ثلاثة أشياء تحتاج نسخاً احتياطياً:

  • ملفات الإعداد في conf والرسائل في prompts وملف Compose وال Dockerfile: هذه نصوص، ومكانها الطبيعي مستودع Git خاص، مع إخراج كلمات المرور منها إلى ملف لا يرفع.
  • ال Volume المسمى spool: فيه البريد الصوتي والتسجيلات وقاعدة AstDB.
  • ال Volume المسمى logs: مفيد للتحقيق بعد حادثة، وأقل أهمية من غيره.

والأمر التالي ينسخ ال Volume إلى ملف مضغوط دون إيقاف المقسم، ثم يعرض بعض ما فيه:

mkdir -p backup
docker run --rm --volumes-from arb-ast-pbx -v "$PWD/backup:/backup" debian:12.15 \
  tar czf /backup/asterisk-spool-$(date +%F).tar.gz -C /var/spool/asterisk .
tar tzf backup/asterisk-spool-$(date +%F).tar.gz | grep -E "astdb|INBOX/msg0000.wav|monitor/.*wav" | head -5

والمخرج سوف يكون كما يلي:

./voicemail/office/1001/INBOX/msg0000.wav
./voicemail/office/1002/INBOX/msg0000.wav
./astdb.sqlite3
./monitor/20261020-084030-1001-1002.wav
./monitor/20261020-083335-1001-1002.wav

ولكي نتأكد من الاستعادة حذفنا كل البريد الصوتي في السياق office، ثم استعدناه من نسخة أخذناها قبل ذلك للمجلدين voicemail وmonitor فقط:

docker exec arb-ast-pbx sh -c 'rm -rf /var/spool/asterisk/voicemail/office'
docker exec arb-ast-pbx asterisk -rx 'voicemail show users' | grep office
docker run --rm --volumes-from arb-ast-pbx -v "$PWD/backup:/backup:ro" debian:12.15 \
  sh -c 'tar xzf /backup/asterisk-spool-test.tar.gz -C /var/spool/asterisk && chown -R 999:999 /var/spool/asterisk/voicemail'
docker exec arb-ast-pbx asterisk -rx 'voicemail show users' | grep office

والمخرج سوف يكون كما يلي:

office     1001  Alice                                     0
office     1002  Bob                                       0
office     1001  Alice                                     2
office     1002  Bob                                       1

والرقم 999 هو رقم المستخدم asterisk داخل ال Image كما يظهر من الأمر docker exec arb-ast-pbx id. وطبق على النسخ الاحتياطية قاعدة 3-2-1 كما في النسخ الاحتياطي بقاعدة 3-2-1 واختبار الاستعادة، ولاحظ أن التسجيلات بيانات حساسة، فشفر النسخة قبل أن تخرج من السيرفر.

التحديث إلى إصدار أحدث

داخل الفرع نفسه يكون التحديث بتغيير رقم الإصدار وإعادة البناء:

  1. اقرأ سجل التغييرات للإصدار الجديد في صفحة التنزيل، وتابع النشرات الأمنية في مستودع المشروع، فهي التي تحدد متى يجب أن تحدث بسرعة.
  2. خذ نسخة احتياطية من ال Volume كما في القسم السابق.
  3. غير ARG AST=22.11.0 في ال Dockerfile ووسم ال Image في ملف Compose إلى الإصدار الجديد، ثم docker compose build وdocker compose up -d.
  4. تحقق ب core show version وبمكالمة تجريبية وبقراءة السجل بحثاً عن ERROR.
  5. للرجوع، أعد الوسم القديم وشغل docker compose up -d، فال Image القديمة ما زالت على الجهاز.

أما الانتقال من فرع إلى فرع، مثل 22 إلى 24 عندما يصدر، فهو أكبر من ذلك، لأن الخيارات القديمة تحذف بين الفروع، كما حذفت chan_sip سابقاً. لذلك اقرأ ملف UPGRADE.txt في الكود المصدري للفرع الجديد، وجرب ملفات إعدادك على Container منفصل قبل الإنتاج، وابحث في سجله عن كل سطر ERROR وWARNING.

قبل أن تنقل هذا إلى الإنتاج

  • غير كل كلمات المرور في هذا المقال إلى قيم عشوائية طويلة، مثل الناتج من openssl rand -base64 18، والسبب أن التحويلة التي تخمن كلمة مرورها تعني مكالمات دولية على حسابك.
  • لا تقم بفتح 5038 أو 8088 على الإنترنت، واجعل bindaddr في manager.conf وhttp.conf على 127.0.0.1 أو عنوان الشبكة الخاصة.
  • افتح منفذ SIP ونطاق RTP في جدار الحماية للعناوين التي تحتاجها فقط، والأفضل أن تصل الهواتف البعيدة عبر WireGuard، وقد شرحنا جدار الحماية على السيرفر في تأمين خادم VPS من أول دخول.
  • اجعل كل سياق لا يصل إلا إلى ما يحتاجه، فالسياق الذي تدخل منه مكالمات الخط الخارجي لا يجب أن يحتوي أي مسار يخرج إلى الخط الخارجي مرة أخرى.
  • راجع اعتبارات الأمان في وثائق Asterisk، وسوف نبني الحماية من الاحتيال وfail2ban وTLS وSRTP في مقال تأمين المقسم ضمن هذه السلسلة.
  • إذا كان المقسم خلف NAT في السحابة فأضف external_media_address وexternal_signaling_address كما في مقال تتبع المكالمة.

مشكلات شائعة وحلولها

العرضالسببالحل
بناء pjproject يفشل ب Error 2 مع make -jالبناء المتوازي للمكتبة المدمجةابن make third-party وحده أولاً
المكالمة إلى هاتف مسجل تفشل ب CHANUNAVAIL بعد إعادة إنشاء ال ContainerAstDB داخل ال Container فضاعت التسجيلاتastdbdir على Volume
كل التحويلات اختفت بعد إعادة التشغيلخيار خاطئ في قالب مشتركاقرأ السجل بعد كل reload وابحث عن Could not create an object
البريد الصوتي لا يحفظ الرسائل القصيرةminsecs ورسالة ترحيب طويلةقصر مدة الرنين أو الرسالة، أو خفض minsecs
رسائل البريد الصوتي لا تصل بالبريد الإلكتروني/usr/sbin/sendmail: not foundأضف msmtp أو Postfix إلى ال Image
ملفات تسجيل لمكالمات لم يرد عليهاMixMonitor قبل Dialالخيار U() في Dial
الرد الآلي يعمل بساعات خاطئةال Container بتوقيت UTCالمنطقة الزمنية في GotoIfTime
الأرقام في القائمة لا تعملطريقة DTMF مختلفة بين الهاتف والمقسمdtmf_mode=rfc4733 وتأكد من إعداد الهاتف
Permission denied في AMIالمستخدم لا يملك فئة الأمرأضف الفئة المطلوبة لهذا المستخدم وحده
Stasis app ... doesn't existبرنامج ARI غير متصلشغل البرنامج، واجعل بعد Stasis مساراً احتياطياً

إزالة التجربة

نزيل ما أنشأناه بالاسم فقط:

docker rm -f arb-ast-bob arb-ast-ari
docker compose down -v
docker rmi arb-asterisk:22.11.0 arb-ast-phone:1

تدريب عملي

  • أضف التحويلة 1003 بثلاثة أقسام قصيرة ترث من القوالب، ثم اجعل الرقم 3 في الرد الآلي يحول إليها، وتأكد ب dialplan show ivr-main.
  • عدل ari-hello.py بحيث يقرأ الرقم الذي يضغطه المتصل من الحدث ChannelDtmfReceived ويطبعه، ثم يحول المكالمة إلى خطة الاتصال بطلب continue.

الخلاصة

  • اختر فرع LTS مكتملاً، واليوم هو 22.11.0، وابنه من الكود المصدري لأن التوزيعات لا تقدم إصداراً حديثاً.
  • القوالب في PJSIP تجعل إضافة موظف ثلاثة أقسام قصيرة، وسطر context هو ما يحدد صلاحيات التحويلة.
  • خطة الاتصال سياقات وامتدادات وأولويات، وDIALSTATUS يحدد ما يحدث بعد الرنين.
  • سجل المكالمات بعد الرد فقط بالخيار U()، وخطط للمساحة بنحو 57.6 MB لكل ساعة WAV.
  • في الرد الآلي استخدم BackGround وعداد محاولات ومنطقة زمنية صريحة.
  • AMI لمراقبة المكالمات وبدئها، وARI لتسليم المكالمة كاملة لبرنامجك، وكلاهما لا يفتح على الإنترنت.
  • بعد كل reload اقرأ السجل، وضع AstDB والبريد والتسجيلات على Volume، واختبر الاستعادة فعلاً.

وفي المقال القادم بإذن الله سوف نثبت FreePBX، ونرى كيف يكتب هذه الملفات نفسها من واجهة الويب.

دليلمصطلحات الاتصالات الهاتفية VoIP للمبتدئينشرح مبسط لمصطلحات الهاتف عبر الإنترنت، وما يمر في الشبكة بين ضغط زر الاتصال ووصول الصوت إلى الطرف الآخر.دليلتتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟نبني مقسماً وهاتفين خلف NAT في تجربة عملية، ونتتبع المكالمة حزمة بحزمة حتى نعرف لماذا يضيع الصوت وكيف نصلحه.دليلشبكات Docker وأفضل الممارسات مع Docker Composeلماذا يبقى المنفذ الذي تنشره في Compose مفتوحاً للإنترنت رغم ufw، وكيف تصل التطبيقات إلى بعضها عبر شبكات Docker دون نشر منافذ.

سجل التحديثات

  • أكتوبر 2026: كتابة الدليل واختباره على Asterisk 22.11.0 مبنياً من الكود المصدري على Debian 12 في Docker، مع هواتف baresip 1.0.0.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.