Questa guida spiega come configurare FreePBX per inviare notifiche push ai client Linphone tramite le API ufficiali Linphone.
Obiettivo: Quando un interno non è raggiungibile, FreePBX invia una push notification per risvegliare l’app Linphone, attende la nuova registrazione del device e solo dopo tenta la consegna della chiamata.
Prerequisiti:
PN_PRID, PN_PARAM e PN_PROVIDER.Di seguito i componenti e il flusso coinvolto.
Le richieste di registrazione vengono intercettate da una subroutine che richiama uno script esterno per recuperare e salvare i parametri push associati all’interno.
; --- 1. Hook per salvare i parametri quando l'app si registra ---
[sub-reg-save-push]
exten => s,1,NoOp(Salvo parametri push per ${EXTEN})
same => n,System(/usr/local/bin/save_pn_params.sh ${EXTEN} &)
same => n,Return()
La subroutine richiama quindi il seguente script shell:
[root@freepbx bin]# cat save_pn_params.sh
#!/bin/bash
EXTEN=$1
PARAM=$2
FILE="/etc/asterisk/pn_cache/${EXTEN}.conf"
if [ -f "$FILE" ]; then
# Trasforma il parametro in MAIUSCOLO e cambia i trattini in underscore
# Esempio: pn-prid -> PN_PRID
SEARCH_STR=$(echo "$PARAM" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
# Cerca nel file e prendi tutto quello che c'è dopo l'uguale
grep -i "^${SEARCH_STR}=" "$FILE" | cut -d'=' -f2- | tr -d '\n'
else
exit 1
fi
Per evitare di perdere i parametri push quando il device non è registrato, viene mantenuta una cache locale. Uno script periodico aggiorna i valori leggendo le informazioni disponibili da Asterisk.
[root@freepbx bin]# cat update_pn_cache.sh
#!/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
EXT=$1
# 1. Tenta di estrarre il PRID attuale da Asterisk
CURRENT_PRID=$(asterisk -rx "pjsip show aor $EXT" | grep -oP "pn-prid=\K[^;> ]+" | head -1)
# 2. SE il PRID non è vuoto, ALLORA aggiorna la cache
if [ ! -z "$CURRENT_PRID" ]; then
# Estrai tutti i parametri
PN_PROVIDER=$(asterisk -rx "pjsip show aor $EXT" | grep -oP "pn-provider=\K[^;> ]+" | head -1)
PN_PARAM=$(asterisk -rx "pjsip show aor $EXT" | grep -oP "pn-param=\K[^;> ]+" | head -1)
# Salva nel file (sovrascrive solo se abbiamo dati nuovi)
echo "pn-prid=$CURRENT_PRID" > /etc/asterisk/pn_cache/$EXT.conf
echo "pn-provider=$PN_PROVIDER" >> /etc/asterisk/pn_cache/$EXT.conf
echo "pn-param=$PN_PARAM" >> /etc/asterisk/pn_cache/$EXT.conf
# Log opzionale per debug
# echo "$(date): Cache aggiornata per $EXT" >> /var/log/asterisk/push_cache.log
else
# Se è vuoto, non fare nulla! Mantieni il file precedente.
exit 0
fi
Questo script viene eseguito per tutti gli interni tramite:
[root@freepbx bin]# cat update_pn_cache_all.sh
#!/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
for ext in $(asterisk -rx "pjsip show endpoints" | grep -oP "^\d+"); do
/usr/local/bin/update_pn_cache.sh $ext
done
L’override della macro serve a:
Questa macro viene usata per le chiamate dirette verso l’interno.
[root@freepbx bin]# cat /etc/asterisk/extensions_override_freepbx.conf
; /etc/asterisk/extensions_override_freepbx.conf
[macro-dial-one]
; ============================================================
; Subroutine 'dstring' con push notification per Linphone
; ============================================================
exten => dstring,1,Set(DSTRING=)
exten => dstring,n,Set(DEVICES=${DB(AMPUSER/${DEXTEN}/device)})
exten => dstring,n,ExecIf($["${DEVICES}"=""]?Return())
exten => dstring,n,ExecIf($["${DEVICES:0:1}"="&"]?Set(DEVICES=${DEVICES:1}))
exten => dstring,n,Set(LOOPCNT=${FIELDQTY(DEVICES,&)})
exten => dstring,n,Set(ITER=1)
exten => dstring,n(begin),Set(THISDIAL=${DB(DEVICE/${CUT(DEVICES,&,${ITER})}/dial)})
exten => dstring,n,GotoIf($["${THISDIAL:0:5}"!="PJSIP"]?docheck)
exten => dstring,n,NoOp(Debug: Found PJSIP Destination ${THISDIAL})
exten => dstring,n,GotoIf($[ ${REGEX("(/.+/|@)" ${THISDIAL})} = 1 ]?doset)
exten => dstring,n,NoOp(Debug: Updating PJSIP Destination with PJSIP_DIAL_CONTACTS)
exten => dstring,n,Set(THISDIAL=${PJSIP_DIAL_CONTACTS(${THISDIAL:6})})
; ============================================================
; PUSH NOTIFICATION: device non registrato ma forse ha push
; ============================================================
exten => dstring,n,GotoIf($["${THISDIAL}"!=""]?docheck)
exten => dstring,n,NoOp(THISDIAL vuoto per ${DEXTEN} - verifico push params)
exten => dstring,n,Set(RAW_PRID=${SHELL(/usr/local/bin/get*pn_params.sh ${DEXTEN} PN_PRID)})
exten => dstring,n,Set(CLEAN_PRID=${FILTER(a-zA-Z0-9.*:-,${RAW_PRID})})
exten => dstring,n,GotoIf($["${CLEAN_PRID}"=""]?docheck)
; Ha i parametri push: mando la notifica
exten => dstring,n,NoOp(Parametri push trovati per ${DEXTEN} - invio push)
exten => dstring,n,Set(RAW_KEY=${SHELL(cat /etc/asterisk/linphone_apikey.conf | cut -d'=' -f2)})
exten => dstring,n,Set(CURR_API_KEY=${FILTER(a-zA-Z0-9,${RAW_KEY})})
exten => dstring,n,Set(RAW_PROV=${SHELL(/usr/local/bin/get_pn_params.sh ${DEXTEN} PN_PROVIDER)})
exten => dstring,n,Set(RAW_PARAM=${SHELL(/usr/local/bin/get_pn_params.sh ${DEXTEN} PN_PARAM)})
exten => dstring,n,Set(CLEAN_PROV=${FILTER(a-zA-Z,${RAW_PROV})})
exten => dstring,n,Set(CLEAN_PARAM=${CUT(RAW_PARAM,&,1)})
exten => dstring,n,Set(CLEAN_CALLID=${FILTER(0-9,${UNIQUEID})})
exten => dstring,n,System(curl -s -X POST https://subscribe.linphone.org/api/push_notification -H "x-api-key: ${CURR_API_KEY}" -H "accept: application/json" -H "content-type: application/json" -d '{"pn_provider":"${CLEAN_PROV}","pn_param":"${CLEAN_PARAM}","pn_prid":"${CLEAN_PRID}","type":"call","call_id":"${CLEAN_CALLID}"}')
; Aspetto che il device si risvegli e si registri
exten => dstring,n,Ringing()
exten => dstring,n,Wait(4)
; Rileggo il contatto aggiornato
exten => dstring,n,Set(THISDIAL=${PJSIP_DIAL_CONTACTS(${DEXTEN})})
exten => dstring,n,GotoIf($["${THISDIAL}"=""]?docheck)
; Pulisco DSTRING dai parametri push (come nel tuo hook originale)
exten => dstring,n,Set(PART1=${CUT(THISDIAL,\;,1)})
exten => dstring,n,Set(PART2=${CUT(THISDIAL,\;,2)})
exten => dstring,n,Set(THISDIAL=${PART1}\;${PART2})
exten => dstring,n,NoOp(THISDIAL aggiornato dopo push: ${THISDIAL})
; ============================================================
exten => dstring,n(docheck),GotoIf($["${THISDIAL}"=""]?skipset)
; AGGIUNGI QUESTE DUE RIGHE:
exten => dstring,n,Set(PART1=${CUT(THISDIAL,\;,1)})
exten => dstring,n,Set(PART2=${CUT(THISDIAL,\;,2)})
exten => dstring,n,Set(THISDIAL=${PART1}\;${PART2})
exten => dstring,n(doset),Set(DSTRING=${DSTRING}${THISDIAL}&)
exten => dstring,n(skipset),Set(ITER=$[${ITER}+1])
exten => dstring,n,GotoIf($[${ITER}<=${LOOPCNT}]?begin)
exten => dstring,n,ExecIf($["${DSTRING:-1}"!="&"]?Return())
exten => dstring,n,Set(DSTRING=${DSTRING:0:$[${LEN(${DSTRING})}-1]})
exten => dstring,n,Return()
Questa macro viene usata per le chiamate dirette verso un ring group.
[macro-dial-ringall-predial-hook]
exten => s,1,NoOp(=== CLEANING LINPHONE PUSH PARAMS FROM DIALSTRING ===)
exten => s,n,GotoIf($["${ds}"=""]?end)
exten => s,n,Set(NEW_DS=)
exten => s,n,Set(LOOPCNT=${FIELDQTY(ds,&)})
exten => s,n,Set(ITER=1)
exten => s,n(check_loop),Set(CHUNK=${CUT(ds,&,${ITER})})
; Prendo la prima parte della stringa prima della barra "/"
exten => s,n,Set(TEST_SLASH=${CUT(CHUNK,/,1)})
; Se CHUNK è identico a TEST_SLASH, significa che NON c'è nessuna "/" (es: non inizia con PJSIP/).
; È spazzatura di Linphone, quindi lo salto.
exten => s,n,GotoIf($["${CHUNK}"="${TEST_SLASH}"]?skip_chunk)
; Se invece c'è la barra, è un dispositivo valido. Lo aggiungo alla nuova stringa.
exten => s,n,Set(NEW_DS=${NEW_DS}${CHUNK}&)
exten => s,n(skip_chunk),Set(ITER=$[${ITER}+1])
exten => s,n,GotoIf($[${ITER}<=${LOOPCNT}]?check_loop)
; Rimuovo l'ultima "&" concatenata dal loop
exten => s,n,ExecIf($["${NEW_DS}"!=""]?Set(NEW_DS=${NEW_DS:0:$[${LEN(${NEW_DS})}-1]}))
exten => s,n,NoOp(Original ds: ${ds})
exten => s,n,NoOp(Cleaned ds : ${NEW_DS})
; Sovrascrivo la variabile "ds" che la macro-dial originale userà nel comando Dial subito dopo
exten => s,n,Set(ds=${NEW_DS})
exten => s,n(end),MacroExit()
Lo script seguente prova prima a leggere i parametri dal contatto SIP attivo. Se non trova nulla, utilizza la cache locale come fallback.
[root@freepbx bin]# cat get_pn_params.sh
#!/bin/bash
EXTEN=$1
PARAM=$2
CACHE_FILE=/etc/asterisk/pn_cache/${EXTEN}.conf
# Prima prova dal contatto live
LIVE=$(asterisk -rx "pjsip show aor $EXTEN" | grep -oP "${PARAM}=\K[^;> ]+" | head -1 | tr -d '\n')
if [ -n "$LIVE" ]; then
echo "$LIVE" | cut -d':' -f1
exit 0
fi
# Fallback: leggi dalla cache
if [ -f "$CACHE_FILE" ]; then
grep "^${PARAM}=" "$CACHE_FILE" | cut -d'=' -f2 | tr -d '\n' | cut -d':' -f1
fi
La API key di Linphone può scadere o diventare non valida dopo un periodo di inattività. Per questo motivo viene eseguito periodicamente uno script che ne richiede una nuova copia e la salva nel file utilizzato da Asterisk.
[root@freepbx bin]# cat renew_linphone_apikey.sh
#!/bin/bash
# /usr/local/bin/renew_linphone_apikey.sh
LINPHONE_USER="sgluca"
LINPHONE_DOMAIN="sip.linphone.org"
LINPHONE_PASS="################"
KEY_FILE="/etc/asterisk/linphone_apikey.conf"
LOG_FILE="/var/log/linphone_apikey.log"
NEW_KEY=$(curl -s \
--digest -u "${LINPHONE_USER}:${LINPHONE_PASS}" \
-X GET "https://subscribe.linphone.org/api/accounts/me/api_key" \
-H "accept: application/json" \
-H "content-type: application/json" \
-H "from: sip:${LINPHONE_USER}@${LINPHONE_DOMAIN}")
if [ -n "$NEW_KEY" ] && [[ "$NEW_KEY" != *"error"* ]] && [[ "$NEW_KEY" != *"invalid"* ]]; then
echo "LINPHONE_API_KEY=${NEW_KEY}" > "$KEY_FILE"
chmod 640 "$KEY_FILE"
chown root:asterisk "$KEY_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S'): Chiave rinnovata OK -> ${NEW_KEY:0:8}..." >> "$LOG_FILE"
echo "Successo! Nuova chiave: ${NEW_KEY:0:8}..."
else
echo "$(date '+%Y-%m-%d %H:%M:%S'): ERRORE rinnovo. Response: $NEW_KEY" >> "$LOG_FILE"
echo "ERRORE! Response: $NEW_KEY"
exit 1
fi
In sintesi, il comportamento è il seguente:
Questo approccio consente di gestire in modo più affidabile i client mobili Linphone, soprattutto quando l’app è in background o il device è temporaneamente sospeso.