AGI-Skript mit Python wertet Caller-IDs eingehender Telefonanrufe aus

21.03.2023

Dieses in Python geschriebene AGI-Skript für Asterisk liest die Caller-IDs (d.h. die Telefonnummern) der eingehenden Gespräche aus, um zum Beispiel Werbeanrufe abzuschrecken oder bekannte Nummern bevorzugt zu behandeln. Die Präfixe der Ortskennzahlen und Ländervorwahlen lassen sich ebenfalls gezielt auswerten. Das Programm entscheidet mit diesen Informationen die weitere Behandlung der Gespräche.

Zum Beispiel kann dieses Skript bestimmte eingehende Nummern bevorzugt zu einem bestimmten Endgerät weiterleiten. Dabei können Nummernblöcke, Ortskennzahlen und Vorwahlen berücksichtigt werden, um Anrufe aus bestimmten Ländern oder Städten entsprechend zu behandeln. Unbekannte Nummern erfahren eine Sprachansage und gelangen zu einem Auswahlmenü. Dies schreckt potenzielle  Werbeanrufe ab.

Es gibt sehr viele Möglichkeiten. Zum Beispiel könnte das Skript auf die Ländervorwahlen reagieren und  zu Auswahlmenüs in den entsprechenden Sprachen umleiten. Es können zum Beispiel auch nur bestimmte Ländervorwahlen durchgelassen werden. Der Rest wird ins Leere geführt. Zu beachten ist, dass Python die if- und elif-Anweisungen von oben nach unten abarbeitet. Sobald ein Bedingung erfüllt ist, wird der entsprechende Anweisung ausgeführt. Was in der if-Schleife danach kommt, kommt nicht mehr zum Zuge. Je spezialisierter eine Regel ist, desto weiter muss sie nach oben angeordnet sein.

Nachfolgend das Skript mit Beispielnummern, um das Prinzip zu verstehen:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 20:22 Montag, 20. März 2023 antiwerbeanrufe.py Volker Lange-Janson

######### Nachfolgendes erspart den Einsatz von pyst #######################

# siehe Beschreibung auf
# http://www.asteriskdocs.org/en/2nd_Edition/asterisk-book-html-chunk/asterisk-CHP-9-SECT-4.html

import sys
import re
import time

# Read and ignore AGI environment (read until blank line)

env = {}
tests = 0;

while 1:
   line = sys.stdin.readline().strip()

   if line == '':
      break
   key,data = line.split(':')
   if key[:4] != 'agi_':
      #skip input that doesn't begin with agi_
      sys.stderr.write("Did not work!\n");
      sys.stderr.flush()
      continue
   key = key.strip()
   data = data.strip()
   if key != '':
      env[key] = data

sys.stderr.write("AGI Environment Dump:\n");
sys.stderr.flush()
for key in env.keys():
   sys.stderr.write(" -- %s = %s\n" % (key, env[key]))
   sys.stderr.flush()

def checkresult (params):
   params = params.rstrip()
   if re.search('^200',params):
      result = re.search('result=(\d+)',params)
      if (not result):
         sys.stderr.write("FAIL ('%s')\n" % params)
         sys.stderr.flush()
         return -1
      else:
         result = result.group(1)
         #debug("Result:%s Params:%s" % (result, params))
         sys.stderr.write("PASS (%s)\n" % result)
         sys.stderr.flush()
         return result
   else:
      sys.stderr.write("FAIL (unexpected result '%s')\n" % params)
      sys.stderr.flush()
      return -2

def sayit (params):
   sys.stderr.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stderr.flush()
   sys.stdout.write("STREAM FILE %s \"\"\n" % str(params))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)

def saynumber (params):
   sys.stderr.write("SAY NUMBER %s \"\"\n" % params)
   sys.stderr.flush()
   sys.stdout.write("SAY NUMBER %s \"\"\n" % params)
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   checkresult(result)

def getnumber (prompt, timelimit, digcount):
   sys.stderr.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stderr.flush()
   sys.stdout.write("GET DATA %s %d %d\n" % (prompt, timelimit, digcount))
   sys.stdout.flush()
   result = sys.stdin.readline().strip()
   result = checkresult(result)
   sys.stderr.write("digits are %s\n" % result)
   sys.stderr.flush()
   if result:
      return result
   else:
      result = -1

################### Ab hier beginnt das eigentliche Skript #####################

# Get the Caller ID from the environment
callerid = env.get('agi_callerid', '')


digitcount=1    # Der Nutzer kann nur eine Ziffer eingeben und danach beginnt die Vermittlung
ttanswer=10000   # Der Nutzer hat 10000 ms ( 10 Sekunden ) die Ziffer oder Ziffern einzugeben


time_duration = 1           # Ein Sekunde Pause eingebaut, damit der Nutzer den Anfang
time.sleep(time_duration)   # der Ansage mitbekommt.


# Diese eingehenden Nummern werden automatisch
# zu den Extensions weitergeleitet.

    
if callerid == "07221987653" :
    # Anruf von Berta Benz
    print("SET VARIABLE EXTNEW " + "1919")
    
elif callerid == "072213456789" :
    # Anruf von Donald Duck
    print("SET VARIABLE EXTNEW " + "1923")
    
elif re.match(r'^(07221)', callerid):
    # Anruf von einer Baden-Badener Nummer mit Vorwahl 07221
    print("SET VARIABLE EXTNEW " + "4088")
    
elif callerid == "089123456789" :
    # Anruf von Dagobert Duck
    print("SET VARIABLE EXTNEW " + "4089")
    
elif callerid == "0897891234" :
    # Anruf von Alfons Habeck
    print("SET VARIABLE EXTNEW " + "3023")
    
elif re.match(r'^(089)', callerid):
    # Anruf alle Nummern München mit der Ortskennzahl 089
    print("SET VARIABLE EXTNEW " + "2088")
    
elif callerid == "0046258123456" :
    # Anruf von Svante Svensson aus Furudal, Schweden
    print("SET VARIABLE EXTNEW " + "2020")
    
elif re.match(r'^(0046)', callerid):
    # Alle Anrufe aus Schweden mit der Länderkennung 0046
    print("SET VARIABLE EXTNEW " + "1046")
    
    
else:
    number = getnumber("/usr/share/asterisk/sounds/eigene/auswahlmenues/weiterleitung123",ttanswer,digitcount)

    if int(number) == 1:   #                                      
        print("SET VARIABLE EXTNEW " + "1923")
        #  Zum Hauptmenue
    elif int(number) == 2: #                       
        print("SET VARIABLE EXTNEW " + "1919")
        
    elif int(number) == 3: #                       
        print("SET VARIABLE EXTNEW " + "5550")
        
    else: 
        print("SET VARIABLE EXTNEW " + "1999")
        

# Auszug aus der extensions.conf

# ; Aufruf der antiwerbeanruf.py mit der 5559
# exten => 5559,1,Noop(Anruf erfolgt von: "${CALLERID(name)}" <${CALLERID(num)}>)
#     same => n,Answer()
#     same => n,Set(callerid=${CALLERID(num)})
#     same => n,AGI(antiwerbeanrufe.py,${callerid})
#     same => n,NoOP(from script antiwerbeanrufe.py dialing ${EXTNEW})
#     same => n,Goto(telefone,${EXTNEW},1)
#     same => n,Hangup()

# Text weiterleitung123 mit https://ttsmp3.com/ Vicki
# <break time="1s"/> Hallo und Herzlich Willkommen bei
# Brunhilde Klein und Otto Schulze. 
# Sie haben folgende Auswahlmöglichkeiten: <break time="1s"/>
# Möchten Sie zu Otto Schulze, drücken Sie die 1. <break time="1s"/>
# Möchten Sie zu Brunhilde Klein, drücken Sie die 2. <break time="1s"/>
# Möchten Sie zum Hauptmenü, drücken Sie die 3. <break time="1s"/>
# Haben Sie keine Möglichkeit Ziffern einzugeben, werden Sie
# nach 10 Sekunden automatisch weitergeleitet.
    

Beschreibung des Skripts: Das Skript “antiwerbeanrufe.py” ist ein Python-Skript, das eine Anwendung eines Asterisk Gateway Interface (AGI) ist. Es wird verwendet, um eingehende Anrufe zu filtern und diese an bestimmte Erweiterungen (Extensions) weiterzuleiten, je nachdem wer anruft.

Das Skript liest die AGI-Umgebung und speichert sie in einem Dictionary mit dem Namen “env”. Es verwendet auch reguläre Ausdrücke, um die Antwort von Asterisk zu überprüfen. Es verwendet die folgenden Funktionen:

  • checkresult(): Überprüft, ob die Antwort von Asterisk eine gültige Antwort ist.
  • sayit(): Spielt eine Audiodatei ab.
  • saynumber(): Spricht eine Nummer aus.
  • getnumber(): Liest die Eingabe des Benutzers ein, während eine prompt abgespielt wird.

Die Zeile

callerid = env.get('agi_callerid', '')

im AGI-Skript dient dazu, die Anrufer-ID (Caller ID) des Anrufers abzurufen und in der Variable callerid zu speichern.

env ist eine Variable im AGI-Skript, die ein Dictionary von Umgebungsvariablen enthält, die von Asterisk an das Skript übergeben werden. agi_callerid ist eine der Umgebungsvariablen, die den Wert der Caller ID enthält.

Die Methode get wird verwendet, um den Wert von agi_callerid abzurufen. Wenn agi_callerid nicht vorhanden ist, wird ein leerer String zurückgegeben, der in der Variable callerid gespeichert wird.

Das Skript verwendet dann die callerid-Variable, um die Caller-ID des Anrufers zu übergeben.

Das Skript definiert eine Liste von Telefonnummern, die bekannt sind. Wenn eine dieser Telefonnummern anruft, wird der Anruf an eine bestimmte Erweiterung direkt ohne den Umweg des Auswahlmenüs weitergeleitet. Dies gilt auch für Nummern, die mit bestimmten Vorwahlen beginnen, zum Beispiel mit Ortskennzahlen der heimischen Stadt, aus der keine Werbeanrufe kommen können. Wenn der Anruf von einer anderen Nummer stammt, wird der Benutzer aufgefordert, eine Nummer einzugeben, die die Weiterleitung bestimmt. Der Benutzer hat 10 Sekunden Zeit, um eine Ziffer oder Ziffern einzugeben. Nach dem verstreichen der 10 Sekunden, wir der Anrufer zu einer bestimmten Nummer weitergeleitet. Dies ist sinnvoll, falls der Anrufer ein Telefon mit Impulswahl betreibt, denn nur mit dem Mehrfrequenzverfahren (Tonwahl, DTMF) lassen sich Ziffern erfolgreich während des Telefonats übertragen.

Der entsprechende  Aufruf der antiwerbeanrufe.py in der extensions.conf: Mit der 5559 wird das Skript aufgerufen. Man kann zum Beispiel dafür sorgen, dass eingehende Anrufe erst einmal auf die 5559 geleitet werden, wodurch die Wahlregeln für die 5559 das AGI-Skript antiwerbeanrufe.py aufrufen und die entsprechenden Parameter übergeben werden. Die 5559 ist natürlich nur ein Beispiel

; Einwahl von zum Beispiel Sipgate
exten => 5559,1,Noop(Anruf erfolgt von: "${CALLERID(name)}" <${CALLERID(num)}>)
    same => n,Answer()
    same => n,Set(callerid=${CALLERID(num)})
    same => n,AGI(antiwerbeanrufe.py,${callerid})
    same => n,NoOP(from script antiwerbeanrufe.py dialing ${EXTNEW})
    same => n,Goto(telefone,${EXTNEW},1)
    same => n,Hangup()

Dieser Aufruf in der Asterisk extensions.conf konfiguriert den Anrufweiterleitungsprozess in der Telefonanlage, wenn die Rufnummer 5559 angerufen wird. Der Anruf wird mit einem Noop-Logeintrag protokolliert, der den Namen und die Rufnummer des Anrufers anzeigt. Anschließend wird der Anruf beantwortet, die Rufnummer des Anrufers wird in der Variablen “callerid” gespeichert und das AGI-Skript antiwerbeanrufe.py wird aufgerufen und die Rufnummer des Anrufers als Parameter übergeben.

Wenn der Anruf erfolgt ist, wird er auf das Ziel-Extension in der Goto-Anweisung weitergeleitet. Der Anruf wird dann auf das angegebene Extension (telefon) und den angegebenen Prioritätslevel (1) weitergeleitet (mit 1 ist die erste Zeile einer Extensions-Anweisung gemeint). Schließlich wird der Anruf beendet (Hangup).

********

Situation in Schweden: In Schweden bleiben privat genutzte Nummern weitgehende verschont, wenn man seine Festnetz- und Mobilfunknummern laut den Anweisungen auf https://nixtelefon.org/ für Werbeanrufe sperren lässt.

Das funktioniert sehr gut. Man ruft eine Nummer von dem Endgerät an, das gesperrt werden soll und folgt ein paar kurzen Anweisungen. Allerdings schützt diese Einrichtung nicht vor ausländischen Anrufen, die sich meistens in betrügerischer Absicht als “Microsoft help desk center” oder “cyber security service” vorstellen, um ahnungslose Menschen abzuzocken, indem sie erklären sie müssten den heimischen PC gegen Geld von Viren bereinigen. Diese Anrufe sind auch sehr stark zurückgegangen, da offenbar so viele darauf nicht mehr eingehen, dass sich das Geschäft nicht mehr lohnt.

Früher war es jedenfalls so, dass oft Menschen im fortgeschrittenen Alter von Werbeanrufen überrumpelt wurden, wenn sie Waldbesitzer waren. Auf dem Land besitzen Privatpersonen oft ein paar Hektar Wald, das sie in ihrer Steuererklärung angeben müssen. Damit sind sie automatisch Gewerbetreibende und die  in der Steuererklärung angegebene Telefonnummer galt als Geschäftsnummer und konnte somit mit NIX nicht gegen Werbeanrufe gesperrt werden. Ob das heute noch so ist, weiß ich nicht.

Ich weiß nur noch, dass unser erster schwedischer Festnetzanschluss (vor 17 Jahren noch so richtig mit einer analogen Zweidrahtleitung) sogleich von Werbeanrufen gestürmt wurde. Pro Tag gab es mindestens 2 bis 4 Werbeanrufe. Heute ist zum Glück Ruhe. Offenbar hat sich auch die schwedische Gesellschaft geändert, die weniger auf  Werbeanrufe reagiert und gelernt hat.

******

Weiterführend und ergänzend:

Der Programmier-Code ist fehlerfrei. Trotzdem läuft das AGI-Skript nicht. Warum verflixt noch einmal?
AGI-Skripte für Asterisk unter Windows editieren – Was ist zu beachten? – 9. März 2023: Windows ist weit verbreitet. Deshalb schreiben nicht wenige ihre AGI-Skripte auf Windows, um sie dann in ihren Raspberry Pi zu kopieren. Wer einige Dinge nicht beachtet, wird die Skripte nicht zum Laufen bekommen. Hier sind die Fallstricke beschrieben, die mich viel Zeit gekostet haben. Die Beispiele beziehen sich auf Python. Sie haben für andere Programmiersprachen die entsprechende Gültigkeit. – weiter

Mein Telefon-Server läuft wartungsfrei rund um die Uhr auf einem Raspberry Pi 3 B+. Die Telefon-Teilnehmer sind weltweit verteilt. Jeder kann mitmachen.
Hurra! Mein Asterisk-Telefon-Server-Tutorial ist fertiggestellt – 9. März 2023: Das Tutorial besteht aus genau 40 Abschnitten und es ermöglicht Anfängern, auf einem Raspberry Pi einen eigenen Telefon-Server zu betreiben und diesen mit anderen Asterisk-Servern zu vernetzen. Zusätzlich sind zahlreiche wirkungsvolle Schutzmaßnahmen wie APIBAN und Fail2Ban beschrieben, um einen sicheren Betrieb zu gewährleisten. Asterisk läuft bei mir dank APIBAN jetzt wartungsfrei rund um die Uhr. Das Tutorial beinhaltet auch die Programmierung von AGI-Skripten auf Python und anderen Programmiersprachen, wodurch es fast keine Grenzen bei der Umsetzung gibt. – weiter