first commit
This commit is contained in:
@@ -0,0 +1,500 @@
|
||||
import de.vertico.starface.module.core.model.VariableType;
|
||||
import de.vertico.starface.module.core.model.Visibility;
|
||||
import de.vertico.starface.module.core.runtime.IBaseExecutable;
|
||||
import de.vertico.starface.module.core.runtime.IRuntimeEnvironment;
|
||||
import de.vertico.starface.module.core.runtime.annotations.Function;
|
||||
import de.vertico.starface.module.core.runtime.annotations.InputVar;
|
||||
import de.vertico.starface.module.core.runtime.annotations.OutputVar;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* BlacklistBlock - STARFACE Custom Block
|
||||
*
|
||||
* Prüft bei jedem eingehenden Anruf die Anrufernummer gegen eine konfigurierbare
|
||||
* Blacklist und führt eine wählbare Aktion aus (auflegen, besetzt, umleiten, Ansage).
|
||||
* Anonyme Anrufe (unterdrückte Rufnummer) werden separat behandelt.
|
||||
*
|
||||
* Das Modul muss als Typ "Call-Processing" mit Aktivierung "bei allen eingehenden
|
||||
* Anrufen" konfiguriert werden, damit der Block im Anruf-Kontext läuft.
|
||||
*
|
||||
* Für STARFACE 8.x, 9.x, 10.x (Java 21)
|
||||
*/
|
||||
@Function(
|
||||
visibility = Visibility.Public,
|
||||
modifiesCalls = true,
|
||||
description = "Prüft eingehende Anrufe gegen eine Blacklist und behandelt anonyme Anrufe"
|
||||
)
|
||||
public class BlacklistBlock implements IBaseExecutable {
|
||||
|
||||
// Voll qualifizierte Namen der STARFACE-internen Blöcke (per Reflection genutzt,
|
||||
// damit der Block über STARFACE-Versionen hinweg robust bleibt).
|
||||
private static final String CN_AGI_RUNTIME =
|
||||
"de.vertico.starface.module.core.runtime.IAGIRuntimeEnvironment";
|
||||
private static final String CN_GET_CALLER =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.GetCaller2";
|
||||
private static final String CN_HANGUP =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.Hangup";
|
||||
private static final String CN_BUSY =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.Busy";
|
||||
private static final String CN_CALL_NUMBER =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.CallPhonenumber2";
|
||||
private static final String CN_ANSWER =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.Answer";
|
||||
private static final String CN_PLAYBACK =
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.audio.PlaybackResourceFile2";
|
||||
|
||||
// ============================================================
|
||||
// INPUT VARIABLEN - werden im Module Designer konfiguriert
|
||||
// ============================================================
|
||||
|
||||
@InputVar(
|
||||
label = "Blacklist-Einträge",
|
||||
description = "Eine Nummer pro Zeile (oder durch Komma getrennt). Pro Zeile optional eine "
|
||||
+ "eigene Aktion und ein Ziel im Format: nummer;AKTION;ziel\n"
|
||||
+ "Beispiele:\n"
|
||||
+ "+49301234567 -> Standard-Aktion\n"
|
||||
+ "0190;BUSY -> dieser Eintrag immer besetzt\n"
|
||||
+ "004989;REDIRECT;100 -> auf Nebenstelle 100 umleiten\n"
|
||||
+ "+4972112345;ALLOW -> Ausnahme (durchlassen)\n"
|
||||
+ "AKTIONEN: HANGUP, BUSY, REDIRECT, ANNOUNCE, ALLOW. "
|
||||
+ "Zeilen mit '#' am Anfang sind Kommentare.",
|
||||
type = VariableType.STRING,
|
||||
guiHints = { "multiline" }
|
||||
)
|
||||
public String blacklistEntries = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Standard-Aktion (Blacklist)",
|
||||
description = "Aktion für Blacklist-Einträge ohne eigene Aktion. "
|
||||
+ "HANGUP = auflegen, BUSY = besetzt, REDIRECT = umleiten, ANNOUNCE = Ansage + auflegen",
|
||||
type = VariableType.STRING,
|
||||
possibleValues = { "HANGUP", "BUSY", "REDIRECT", "ANNOUNCE" }
|
||||
)
|
||||
public String defaultAction = "HANGUP";
|
||||
|
||||
@InputVar(
|
||||
label = "Standard-Umleitungsziel",
|
||||
description = "Zielrufnummer/Nebenstelle für REDIRECT-Einträge ohne eigenes Ziel (z.B. 100 oder +4972199999)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String defaultRedirectTarget = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Ansage-Datei",
|
||||
description = "Name der hochgeladenen Sound-Ressource für die Aktion ANNOUNCE (ohne Pfad). "
|
||||
+ "Die Datei muss als Ressource im Modul hinterlegt sein.",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String announcementFile = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Aktion bei anonymen Anrufen",
|
||||
description = "Behandlung von Anrufen mit unterdrückter Rufnummer. "
|
||||
+ "NONE = nichts tun (Anruf normal durchstellen), sonst HANGUP/BUSY/REDIRECT/ANNOUNCE",
|
||||
type = VariableType.STRING,
|
||||
possibleValues = { "NONE", "HANGUP", "BUSY", "REDIRECT", "ANNOUNCE" }
|
||||
)
|
||||
public String anonymousAction = "NONE";
|
||||
|
||||
@InputVar(
|
||||
label = "Umleitungsziel für anonyme Anrufe",
|
||||
description = "Zielrufnummer/Nebenstelle, falls die Aktion für anonyme Anrufe REDIRECT ist",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String anonymousRedirectTarget = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Ländervorwahl",
|
||||
description = "Eigene Ländervorwahl ohne führende Null/Plus (Deutschland = 49). "
|
||||
+ "Wird genutzt, um nationale Nummern (0721...) und internationale (+49721...) einheitlich zu vergleichen.",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String countryCode = "49";
|
||||
|
||||
@InputVar(
|
||||
label = "Präfix-/Bereichssperre erlauben",
|
||||
description = "Wenn aktiv, sperrt ein Eintrag auch alle Nummern, die mit ihm beginnen "
|
||||
+ "(z.B. '0190' sperrt den gesamten Bereich). Wenn aus, wird nur exakt verglichen.",
|
||||
type = VariableType.BOOLEAN
|
||||
)
|
||||
public Boolean prefixMatch = true;
|
||||
|
||||
// ============================================================
|
||||
// OUTPUT VARIABLEN - Ergebnis der Prüfung
|
||||
// ============================================================
|
||||
|
||||
@OutputVar(
|
||||
label = "Anrufernummer",
|
||||
description = "Die erkannte Anrufernummer (international normalisiert)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String caller = "";
|
||||
|
||||
@OutputVar(
|
||||
label = "Anonym",
|
||||
description = "true, wenn der Anrufer die Rufnummer unterdrückt hat",
|
||||
type = VariableType.BOOLEAN
|
||||
)
|
||||
public Boolean isAnonymous = false;
|
||||
|
||||
@OutputVar(
|
||||
label = "Geblockt",
|
||||
description = "true, wenn der Anruf durch die Blacklist oder die Anonym-Regel behandelt wurde",
|
||||
type = VariableType.BOOLEAN
|
||||
)
|
||||
public Boolean blocked = false;
|
||||
|
||||
@OutputVar(
|
||||
label = "Ausgeführte Aktion",
|
||||
description = "Die tatsächlich ausgeführte Aktion (HANGUP/BUSY/REDIRECT/ANNOUNCE/ALLOW/NONE)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String actionTaken = "NONE";
|
||||
|
||||
@OutputVar(
|
||||
label = "Status",
|
||||
description = "Status-Meldung der Prüfung",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String statusMessage = "";
|
||||
|
||||
// Runtime
|
||||
private IRuntimeEnvironment runtime;
|
||||
private org.apache.logging.log4j.Logger log;
|
||||
|
||||
// Ermittelte Anrufer-Daten (statt innerer Klasse, um separate .class-Dateien zu vermeiden)
|
||||
private String callerRaw = "";
|
||||
private String callerNameVal = "";
|
||||
|
||||
@Override
|
||||
public void execute(IRuntimeEnvironment runtime) throws Exception {
|
||||
this.runtime = runtime;
|
||||
this.log = runtime.getLog();
|
||||
|
||||
// Sicherstellen, dass wir im Anruf-Kontext laufen (AGI-Runtime)
|
||||
Class<?> agiClass;
|
||||
try {
|
||||
agiClass = Class.forName(CN_AGI_RUNTIME);
|
||||
} catch (ClassNotFoundException e) {
|
||||
statusMessage = "Fehler: AGI-Runtime nicht verfügbar";
|
||||
log.error("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
if (!agiClass.isInstance(runtime)) {
|
||||
statusMessage = "Übersprungen - kein Anruf-Kontext (Modul muss 'Call-Processing' / "
|
||||
+ "'bei allen eingehenden Anrufen' sein)";
|
||||
log.warn("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Anrufer-Informationen ermitteln (füllt callerRaw, callerNameVal, caller, isAnonymous)
|
||||
if (!getCaller()) {
|
||||
statusMessage = "Anrufer konnte nicht ermittelt werden - Anruf wird durchgestellt";
|
||||
log.warn("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Blacklist: Eingehender Anruf - Nummer='" + callerRaw
|
||||
+ "' (intl='" + caller + "'), anonym=" + isAnonymous
|
||||
+ ", Name='" + callerNameVal + "'");
|
||||
|
||||
// 2. Anonyme Anrufe separat behandeln
|
||||
if (Boolean.TRUE.equals(isAnonymous)) {
|
||||
String act = norm(anonymousAction);
|
||||
if (act.isEmpty() || act.equals("NONE") || act.equals("ALLOW")) {
|
||||
statusMessage = "Anonymer Anruf - keine Aktion, wird durchgestellt";
|
||||
actionTaken = "NONE";
|
||||
log.info("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
performAction(act, anonymousRedirectTarget);
|
||||
blocked = true;
|
||||
statusMessage = "Anonymer Anruf -> " + actionTaken;
|
||||
log.info("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Nummer gegen Blacklist prüfen.
|
||||
// matchBlacklist() füllt matchAction/matchTarget/matchEntry, Rückgabe = Treffer ja/nein.
|
||||
if (!matchBlacklist(caller)) {
|
||||
statusMessage = "Nummer nicht in Blacklist - wird durchgestellt";
|
||||
actionTaken = "NONE";
|
||||
log.info("Blacklist: " + statusMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchAction.equals("ALLOW")) {
|
||||
statusMessage = "Treffer auf Ausnahme (ALLOW) - wird durchgestellt";
|
||||
actionTaken = "ALLOW";
|
||||
log.info("Blacklist: " + statusMessage + " [Regel: " + matchEntry + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
performAction(matchAction, !matchTarget.isEmpty() ? matchTarget : defaultRedirectTarget);
|
||||
blocked = true;
|
||||
statusMessage = "Blacklist-Treffer [" + matchEntry + "] -> " + actionTaken;
|
||||
log.info("Blacklist: " + statusMessage);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Anrufer ermitteln über GetCaller2
|
||||
// ============================================================
|
||||
|
||||
/** Füllt callerRaw, callerNameVal, caller (intl) und isAnonymous. Rückgabe false bei Fehler. */
|
||||
private boolean getCaller() {
|
||||
try {
|
||||
Object getCaller = newBlock(CN_GET_CALLER);
|
||||
execBlock(getCaller);
|
||||
|
||||
callerNameVal = strField(getCaller, "callerName");
|
||||
|
||||
Object anon = field(getCaller, "isAnonymous");
|
||||
isAnonymous = (anon instanceof Boolean) && (Boolean) anon;
|
||||
|
||||
// Externe Nummer bevorzugen, dann Signalisierung, dann interne Nummer
|
||||
String ext = strField(getCaller, "callerExtNumber");
|
||||
String sig = strField(getCaller, "callerSignallingNumber");
|
||||
String intn = strField(getCaller, "callerIntNumber");
|
||||
|
||||
callerRaw = !ext.isEmpty() ? ext : (!sig.isEmpty() ? sig : intn);
|
||||
caller = toIntl(callerRaw);
|
||||
|
||||
// Wenn keinerlei Nummer da ist, gilt der Anruf praktisch als anonym
|
||||
if (callerRaw.isEmpty() && !isAnonymous) {
|
||||
log.debug("Blacklist: Keine Anrufernummer vorhanden - als anonym behandelt");
|
||||
isAnonymous = true;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Blacklist: Fehler beim Ermitteln des Anrufers: " + e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Blacklist-Matching
|
||||
// ============================================================
|
||||
|
||||
// Ergebnis des letzten Treffers (statt innerer Klasse)
|
||||
private String matchAction = "";
|
||||
private String matchTarget = "";
|
||||
private String matchEntry = "";
|
||||
|
||||
/** Prüft callerIntl gegen die Einträge. Bei Treffer werden matchAction/Target/Entry gesetzt. */
|
||||
private boolean matchBlacklist(String callerIntl) {
|
||||
if (blacklistEntries == null || blacklistEntries.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (callerIntl == null || callerIntl.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trennung an Zeilenumbrüchen und Kommas
|
||||
String[] lines = blacklistEntries.split("[\\r\\n,]+");
|
||||
for (String line : lines) {
|
||||
String entry = line.trim();
|
||||
if (entry.isEmpty() || entry.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = entry.split(";");
|
||||
String number = parts[0].trim();
|
||||
String action = parts.length > 1 ? norm(parts[1]) : norm(defaultAction);
|
||||
String target = parts.length > 2 ? parts[2].trim() : "";
|
||||
|
||||
if (action.isEmpty()) {
|
||||
action = "HANGUP";
|
||||
}
|
||||
|
||||
String entryIntl = toIntl(number);
|
||||
if (entryIntl.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean hit;
|
||||
if (callerIntl.equals(entryIntl)) {
|
||||
hit = true;
|
||||
} else if (Boolean.TRUE.equals(prefixMatch)
|
||||
&& entryIntl.length() >= 3
|
||||
&& callerIntl.startsWith(entryIntl)) {
|
||||
hit = true;
|
||||
} else {
|
||||
hit = false;
|
||||
}
|
||||
|
||||
if (hit) {
|
||||
matchAction = action;
|
||||
matchTarget = target;
|
||||
matchEntry = entry;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Aktionen ausführen
|
||||
// ============================================================
|
||||
|
||||
private void performAction(String action, String target) {
|
||||
try {
|
||||
switch (action) {
|
||||
case "HANGUP":
|
||||
case "REJECT":
|
||||
execBlock(newBlock(CN_HANGUP));
|
||||
actionTaken = "HANGUP";
|
||||
break;
|
||||
|
||||
case "BUSY":
|
||||
execBlock(newBlock(CN_BUSY));
|
||||
actionTaken = "BUSY";
|
||||
break;
|
||||
|
||||
case "REDIRECT":
|
||||
case "FORWARD":
|
||||
if (target == null || target.trim().isEmpty()) {
|
||||
log.error("Blacklist: REDIRECT ohne Ziel - lege stattdessen auf");
|
||||
execBlock(newBlock(CN_HANGUP));
|
||||
actionTaken = "HANGUP";
|
||||
} else {
|
||||
redirect(target.trim());
|
||||
actionTaken = "REDIRECT";
|
||||
}
|
||||
break;
|
||||
|
||||
case "ANNOUNCE":
|
||||
announce();
|
||||
actionTaken = "ANNOUNCE";
|
||||
break;
|
||||
|
||||
default:
|
||||
log.warn("Blacklist: Unbekannte Aktion '" + action + "' - lege auf");
|
||||
execBlock(newBlock(CN_HANGUP));
|
||||
actionTaken = "HANGUP";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Blacklist: Fehler beim Ausführen der Aktion '" + action + "': " + e.getMessage(), e);
|
||||
// Im Zweifel auflegen
|
||||
try {
|
||||
execBlock(newBlock(CN_HANGUP));
|
||||
actionTaken = "HANGUP";
|
||||
} catch (Exception ignore) {
|
||||
// nichts mehr möglich
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void redirect(String target) throws Exception {
|
||||
Object call = newBlock(CN_CALL_NUMBER);
|
||||
setField(call, "phoneNumber", target);
|
||||
// Ursprünglichen Anrufer mitsignalisieren, damit das Ziel sieht, wer anruft
|
||||
setField(call, "callerName", callerNameVal != null ? callerNameVal : "");
|
||||
setField(call, "callerNumber", callerRaw != null ? callerRaw : "");
|
||||
setField(call, "timeout", 30);
|
||||
execBlock(call);
|
||||
log.info("Blacklist: Anruf von " + caller + " umgeleitet auf " + target);
|
||||
}
|
||||
|
||||
private void announce() throws Exception {
|
||||
// Anruf annehmen, Ansage abspielen, dann auflegen
|
||||
Object answer = newBlock(CN_ANSWER);
|
||||
setField(answer, "delay", 0);
|
||||
execBlock(answer);
|
||||
|
||||
if (announcementFile != null && !announcementFile.trim().isEmpty()) {
|
||||
Object playback = newBlock(CN_PLAYBACK);
|
||||
setField(playback, "defaultFile", announcementFile.trim());
|
||||
execBlock(playback);
|
||||
} else {
|
||||
log.warn("Blacklist: ANNOUNCE ohne Ansage-Datei konfiguriert");
|
||||
}
|
||||
|
||||
execBlock(newBlock(CN_HANGUP));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Reflection-Hilfen
|
||||
// ============================================================
|
||||
|
||||
private Object newBlock(String className) throws Exception {
|
||||
return Class.forName(className).getDeclaredConstructor().newInstance();
|
||||
}
|
||||
|
||||
/** Führt einen Call-Handling-Block mit der aktuellen AGI-Runtime aus. */
|
||||
private void execBlock(Object block) throws Exception {
|
||||
Class<?> agiClass = Class.forName(CN_AGI_RUNTIME);
|
||||
Method m = block.getClass().getMethod("execute", agiClass);
|
||||
m.invoke(block, runtime);
|
||||
}
|
||||
|
||||
private void setField(Object obj, String name, Object value) {
|
||||
try {
|
||||
Field f = obj.getClass().getField(name);
|
||||
f.set(obj, value);
|
||||
} catch (NoSuchFieldException e) {
|
||||
log.debug("Blacklist: Feld '" + name + "' nicht vorhanden in " + obj.getClass().getName());
|
||||
} catch (Exception e) {
|
||||
log.warn("Blacklist: Konnte Feld '" + name + "' nicht setzen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Object field(Object obj, String name) {
|
||||
try {
|
||||
Field f = obj.getClass().getField(name);
|
||||
return f.get(obj);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String strField(Object obj, String name) {
|
||||
Object v = field(obj, name);
|
||||
return v != null ? v.toString() : "";
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Nummern-Normalisierung
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Normalisiert eine Rufnummer in eine einheitliche internationale Ziffernform.
|
||||
* Beispiele (countryCode=49):
|
||||
* +49 721 12345 -> 4972112345
|
||||
* 0049 721 12345 -> 4972112345
|
||||
* 0721 12345 -> 4972112345
|
||||
* 100 -> 100 (interne Nebenstelle)
|
||||
*/
|
||||
private String toIntl(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String r = raw.trim();
|
||||
if (r.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
boolean plus = r.startsWith("+");
|
||||
String digits = r.replaceAll("[^0-9]", "");
|
||||
if (digits.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String cc = countryCode != null ? countryCode.replaceAll("[^0-9]", "") : "";
|
||||
if (plus) {
|
||||
return digits;
|
||||
}
|
||||
if (digits.startsWith("00")) {
|
||||
return digits.substring(2);
|
||||
}
|
||||
if (digits.startsWith("0")) {
|
||||
return cc + digits.substring(1);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
private String norm(String s) {
|
||||
return s == null ? "" : s.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user