في مقال تتبع المكالمة استخدمنا Asterisk كأداة فقط: تحويلتان وسطران في خطة الاتصال، ثم ركزنا على ما يحدث للحزم في الشبكة. ولكن المقسم في أي مكتب حقيقي يطلب منه أكثر من ذلك بكثير، فالموظف الذي لا يرد يجب أن يترك له المتصل رسالة، والعميل الذي يتصل بالرقم الرئيسي يسمع قائمة «للمبيعات اضغط 1»، والإدارة تريد تسجيل المكالمات، ونظام CRM يريد أن يعرف من يتصل الآن وأن يبدأ مكالمة بضغطة زر. وكل هذا يكتب في Asterisk في ملفات نصية، وأغلب من يفتحها أول مرة يغلقها بسرعة، والسبب أن الوثائق ضخمة ولا تقول لك من أين تبدأ.
المواضيع في السلسلة:
- مصطلحات الاتصالات الهاتفية VoIP للمبتدئين
- تتبع مكالمة SIP عبر الشبكة: لماذا يصل الصوت في اتجاه واحد؟
- لماذا يتقطع الصوت في مكالمات VoIP؟ قياس Jitter وPacket Loss
- ربط المقسم PBX بشبكة الهاتف عبر SIP Trunk والأرقام DID
- دليل Asterisk العملي: من التحويلة Extension إلى الرد الآلي IVR (الموضوع الحالي)
- FreePBX: مقسم هاتف PBX كامل تديره من المتصفح
- FreeSWITCH للمنصات الكبيرة ومراكز الاتصال Contact Center
- 3CX أم FreePBX أم FreeSWITCH؟ كيف تختار المقسم PBX
- هواتف SIP المكتبية والتطبيقات: لماذا لا يرن الجوال النائم؟
- مركز اتصال Call Center على Asterisk: الطابور وتقرير SLA
- تأمين المقسم من الاحتيال على المكالمات Toll Fraud
- موازنة الحمل Load Balancing أمام خوادم Asterisk مع Kamailio
لذلك سوف نبني في هذا المقال مقسماً كاملاً خطوة بخطوة، وكل خطوة تبنى على التي قبلها، وكل مخرج في المقال جاء من تشغيل حقيقي. وسوف نتناول ما يلي:
- ما هو 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 | الحالة |
|---|---|---|---|---|---|
| 20 | LTS | 2022-10-19 | 2026-10-19 | 2027-10-19 | دخل مرحلة إصلاحات الأمان |
| 21 | Standard | 2023-10-18 | 2025-10-18 | 2026-10-18 | انتهى دعمه |
| 22 | LTS | 2024-10-16 | 2028-10-16 | 2029-10-16 | دعم كامل |
| 23 | Standard | 2025-10-15 | 2026-10-15 | 2027-10-15 | دعم كامل بحسب الجدول |
| 24 | LTS | 2026-10-15 (مخطط) | 2030-10-15 | 2031-10-15 | إصدار تجريبي RC |
ولاحظ أن الفرع 24 موعده في الجدول 15 أكتوبر 2026، ولكن آخر ما نشر منه في صفحة التنزيل عند كتابة المقال هو 24.0.0-rc2 بتاريخ 8 أكتوبر، أي أنه لم يصدر بشكل نهائي بعد. لذلك اخترنا في هذا الدليل الإصدار 22.11.0، وهو آخر إصدار نهائي من فرع 22 LTS، صدر في 27 أغسطس 2026 كما في سجل التغييرات، ودعمه الكامل مستمر حتى أكتوبر 2028. والقاعدة التي ننصح بها هي ألا تقوم بتثبيت فرع LTS جديد في الإنتاج في أول أسابيعه، والسبب أن الإصدارات الأولى من كل فرع تحمل أغلب الأخطاء، فابدأ اليوم بفرع 22، وخطط للانتقال إلى 24 بعد عدة إصدارات تصحيحية منه.
كيف يعمل Asterisk من الداخل
قبل أن نكتب أي ملف، لنفهم الأجزاء التي سوف نتعامل معها، والصورة التالية تبين ما يحدث داخل Asterisk عندما تصل المكالمة:

- القناة 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_USAstDB قاعدة 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.
والصورة التالية تبين ما سوف نبنيه في هذا القسم: تسجيل المكالمة، ثم الرنين، ثم البريد الصوتي بحسب سبب عدم الرد:

تسجيل المكالمات 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 foundAsterisk لا يرسل البريد بنفسه، وإنما يستدعي البرنامج /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». والصورة التالية تبين القائمة التي سوف نبنيها:

وهذا هو الإعداد كاملاً، أضفناه إلى 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 واختبار الاستعادة، ولاحظ أن التسجيلات بيانات حساسة، فشفر النسخة قبل أن تخرج من السيرفر.
التحديث إلى إصدار أحدث
داخل الفرع نفسه يكون التحديث بتغيير رقم الإصدار وإعادة البناء:
- اقرأ سجل التغييرات للإصدار الجديد في صفحة التنزيل، وتابع النشرات الأمنية في مستودع المشروع، فهي التي تحدد متى يجب أن تحدث بسرعة.
- خذ نسخة احتياطية من ال Volume كما في القسم السابق.
- غير
ARG AST=22.11.0في ال Dockerfile ووسم ال Image في ملف Compose إلى الإصدار الجديد، ثمdocker compose buildوdocker compose up -d. - تحقق ب
core show versionوبمكالمة تجريبية وبقراءة السجل بحثاً عنERROR. - للرجوع، أعد الوسم القديم وشغل
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 بعد إعادة إنشاء ال Container | AstDB داخل ال 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.