commit a5b1de275d3cbe91d87cde144d9e007466553f47 Author: Stefan Hacker Date: Tue Jun 9 10:17:37 2026 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d285e1f --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Blacklist für STARFACE + +Custom Block für den STARFACE Module Designer: prüft **eingehende Anrufe** gegen eine +konfigurierbare Blacklist und behandelt **anonyme Anrufe** (unterdrückte Rufnummer). +Pro Eintrag lässt sich festlegen, was passieren soll – auflegen, besetzt, umleiten oder +Ansage abspielen. + +## Was kann das Modul? + +- **Pro Nummer / Bereich** eine eigene Aktion: `HANGUP`, `BUSY`, `REDIRECT`, `ANNOUNCE`, `ALLOW` +- **Anonyme Anrufer** (Rufnummernunterdrückung) separat behandeln +- **Bereichssperren** (z.B. ganzer `0190`-Bereich) inklusive **Ausnahmen** (`ALLOW`) +- Robuste **Nummern-Normalisierung** (+49 / 0049 / 0… werden einheitlich verglichen) + +## Quick Start (selbst kompilieren) + +```bash +cd v8-9-10 +./build-block.sh # erzeugt BlacklistBlock.class +``` + +Die STARFACE-API-JARs werden automatisch aus dem benachbarten `mail2fax`-Modul genutzt, +falls sie nicht in `v8-9-10/libs/starface/` liegen. Es sind **keine** Zusatz-Bibliotheken nötig. + +Danach im Module Designer importieren → siehe **[v8-9-10/INSTALLATION.md](v8-9-10/INSTALLATION.md)**. + +## Projektstruktur + +``` +blacklist-module/ +├── README.md +└── v8-9-10/ # für STARFACE 8/9/10 + ├── BlacklistBlock.java # Custom Block Quellcode + ├── BlacklistBlock.class # kompiliertes Ergebnis + ├── build-block.sh # Kompilier-Script + ├── README.md # technische Doku (Felder, Aktionen, Matching) + ├── INSTALLATION.md # Schritt-für-Schritt im Module Designer + └── libs/starface/ # STARFACE API JARs (für den Build) +``` + +## Unterschied zum Mail2Fax-Modul + +| | Mail2Fax | Blacklist | +|--|----------|-----------| +| Auslöser | **Timer** (pollt regelmäßig E-Mails) | **eingehender Anruf** (Call-Processing) | +| Runtime | normale Runtime | AGI-Runtime (Anruf-Kontext) | +| Zusatz-Libs | JavaMail erforderlich | keine | +| Aktion | Fax senden | Anruf auflegen/besetzt/umleiten/Ansage | + +## Wichtig zur Einbindung + +Das Modul muss als **Call-Processing**-Modul mit Aktivierung **"bei allen eingehenden +Anrufen"** eingebunden werden (oder gezielt in die Rufverteilung eines Benutzers/einer +Gruppe). Nur dann läuft der Block im Anruf-Kontext und kann den Anruf beeinflussen. +Details in [v8-9-10/INSTALLATION.md](v8-9-10/INSTALLATION.md). + diff --git a/v8-9-10/BlacklistBlock.class b/v8-9-10/BlacklistBlock.class new file mode 100644 index 0000000..1f293b6 Binary files /dev/null and b/v8-9-10/BlacklistBlock.class differ diff --git a/v8-9-10/BlacklistBlock.java b/v8-9-10/BlacklistBlock.java new file mode 100644 index 0000000..b9c8b61 --- /dev/null +++ b/v8-9-10/BlacklistBlock.java @@ -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(); + } +} diff --git a/v8-9-10/INSTALLATION.md b/v8-9-10/INSTALLATION.md new file mode 100644 index 0000000..acb7766 --- /dev/null +++ b/v8-9-10/INSTALLATION.md @@ -0,0 +1,156 @@ +# Blacklist - Installations- und Konfigurationshandbuch + +Schritt-für-Schritt-Anleitung zur Installation des Blacklist-Moduls im STARFACE Module Designer. + +--- + +## Voraussetzungen + +- STARFACE 8.x, 9.x oder 10.x +- Admin-Zugriff auf STARFACE +- Kompilierte `BlacklistBlock.class` (siehe `./build-block.sh`) + +> Es ist **keine** zusätzliche Bibliothek nötig (anders als bei Mail2Fax kein JavaMail). +> Es wird auch **kein** SSH-Zugriff zum Installieren von Libs benötigt. + +--- + +## Teil 1: Modul-Erstellung im Module Designer + +### Schritt 1: Neues Modul erstellen + +STARFACE Admin → **Module → Module Designer → Neues Modul** + +- **Modul-Name**: `Blacklist` +- **Beschreibung**: "Sperrt/behandelt eingehende Anrufe anhand einer Blacklist und anonyme Anrufe" +- **Version**: `1.0.0` + +### Schritt 2: Class-Datei hochladen + +Tab **Ressourcen** → **Datei hochladen** → `BlacklistBlock.class` + +Optional (nur für die Aktion `ANNOUNCE`): eine Ansage-Audiodatei (WAV) hochladen. +Der Dateiname wird später im Feld **Ansage-Datei** eingetragen. + +### Schritt 3: Funktion erstellen + +Tab **Funktionen** → neue Funktion `Blacklist` anlegen und den Block `BlacklistBlock` +in den Ablauf ziehen. + +### Schritt 4: GUI-Elemente (Eingabefelder) anlegen + +Lege die Konfigurationsfelder an und verknüpfe sie mit den `@InputVar`-Feldern des Blocks: + +| Feldname | Typ | Label | Auswahlwerte | +|----------|-----|-------|--------------| +| `blacklistEntries` | STRING (mehrzeilig) | Blacklist-Einträge | | +| `defaultAction` | STRING | Standard-Aktion | HANGUP, BUSY, REDIRECT, ANNOUNCE | +| `defaultRedirectTarget` | STRING | Standard-Umleitungsziel | | +| `announcementFile` | STRING | Ansage-Datei | | +| `anonymousAction` | STRING | Aktion bei anonymen Anrufen | NONE, HANGUP, BUSY, REDIRECT, ANNOUNCE | +| `anonymousRedirectTarget` | STRING | Umleitungsziel anonyme Anrufe | | +| `countryCode` | STRING | Ländervorwahl | (Default 49) | +| `prefixMatch` | BOOLEAN | Bereichssperre erlauben | (Default an) | + +> Die Auswahlwerte (`possibleValues`) sind bereits im Block hinterlegt und erscheinen +> i.d.R. automatisch als Dropdown. Falls nicht, als normales Textfeld anlegen. + +### Schritt 5: GUI-Felder zuweisen + +Jedes GUI-Element mit dem gleichnamigen Block-Parameter verbinden. Alle `@InputVar`-Felder +sollten verknüpft sein. + +### Schritt 6: Modul speichern + +Modul speichern. + +--- + +## Teil 2: Modul als Anruf-Modul aktivieren + +Damit der Block bei eingehenden Anrufen läuft, muss das Modul als **Call-Processing-Modul** +eingebunden werden. + +### Variante A: Für alle eingehenden Anrufe + +1. Module → **Modulkonfigurationen** → **[+] Neu** +2. Modul `Blacklist` auswählen, Instanz benennen (z.B. "Blacklist - Zentrale") +3. Felder ausfüllen (siehe Teil 3) +4. Bei den Modul-Einstellungen den Typ **Call-Processing** wählen und die Aktivierung auf + **"bei allen eingehenden Anrufen"** setzen. +5. Modul **aktivieren**. + +### Variante B: Pro Benutzer/Gruppe (Rufverteilung) + +Alternativ kann das Modul gezielt in die **Rufverteilung** eines Benutzers oder einer +Gruppe eingehängt werden (Administration → Benutzer/Gruppe → Rufverteilung → Modul +einfügen, **vor** der eigentlichen Zustellung). So gilt die Blacklist nur für diese +Nummern. + +> **Wichtig:** Bei einem Treffer mit Aktion `HANGUP`/`BUSY`/`REDIRECT`/`ANNOUNCE` wird der +> Anruf beendet bzw. umgeleitet und erreicht die nachgelagerte Zustellung nicht mehr. +> Bei `ALLOW`/`NONE`/keinem Treffer läuft der Anruf normal weiter. + +--- + +## Teil 3: Konfiguration (Beispiel) + +**Blacklist-Einträge:** +``` +# Werbeanrufe-Bereiche komplett sperren +0190;BUSY +0900;BUSY +# einzelner Störer: auflegen ++49301234567;HANGUP +# Ausnahme: dieser eine Anschluss aus dem 089-Raum ist ok ++4989123456;ALLOW +089;ANNOUNCE +# nervige Nummer auf die Zentrale umleiten ++4972198765;REDIRECT;100 +``` + +**Standard-Aktion (Blacklist):** `HANGUP` +**Standard-Umleitungsziel:** `100` +**Ansage-Datei:** `blacklist_ansage.wav` (zuvor als Ressource hochgeladen) + +**Aktion bei anonymen Anrufen:** `BUSY` +*(oder `REDIRECT` mit Ziel, oder `NONE` zum Durchstellen)* + +**Ländervorwahl:** `49` +**Bereichssperre erlauben:** ✓ an + +Mit dieser Konfiguration: +- `0190…`/`0900…`/anonyme Anrufer hören Besetzt +- `+49 30 1234567` wird aufgelegt +- der gesamte `089…`-Bereich bekommt eine Ansage – **außer** `+49 89 123456` (Ausnahme oben) +- `+49 721 98765` wird auf Nebenstelle 100 umgeleitet + +--- + +## Logs und Fehlersuche + +**Log-Datei:** +``` +/var/log/starface/starface.log +``` + +**Nützliche Meldungen:** +```bash +# alle Blacklist-Entscheidungen +grep "Blacklist:" /var/log/starface/starface.log + +# Treffer +grep "Blacklist-Treffer" /var/log/starface/starface.log + +# anonyme Anrufe +grep "Blacklist: Anonymer Anruf" /var/log/starface/starface.log +``` + +**Häufige Punkte:** +1. Block tut nichts / "kein Anruf-Kontext": Das Modul ist nicht als *Call-Processing* / + "bei allen eingehenden Anrufen" eingebunden. +2. REDIRECT ohne Wirkung: Zielrufnummer/Nebenstelle prüfen, ggf. mit Amtskennziffer. +3. ANNOUNCE ohne Ton: Audiodatei als Ressource hochgeladen und Dateiname im Feld korrekt? +4. Nummer wird nicht erkannt: `countryCode` prüfen und im Log die normalisierte + `intl='…'`-Form mit dem Eintrag vergleichen. + diff --git a/v8-9-10/README.md b/v8-9-10/README.md new file mode 100644 index 0000000..d4c91e9 --- /dev/null +++ b/v8-9-10/README.md @@ -0,0 +1,113 @@ +# BlacklistBlock - STARFACE Custom Block + +Ein Custom Block für den STARFACE Module Designer, der eingehende Anrufe gegen eine +konfigurierbare Blacklist prüft und anonyme Anrufe (unterdrückte Rufnummer) separat +behandelt. Pro Eintrag kann festgelegt werden, **was** mit dem Anruf passieren soll. + +## Funktionsweise + +Der Block wird **bei jedem eingehenden Anruf** ausgeführt (Modultyp *Call-Processing*). +Ablauf: + +1. `GetCaller2` ermittelt Anrufernummer, Name und das Flag `isAnonymous`. +2. **Anonymer Anruf** (unterdrückte Rufnummer): es greift die *Aktion bei anonymen Anrufen*. +3. Andernfalls wird die Nummer gegen die **Blacklist-Einträge** geprüft (erster Treffer gewinnt). +4. Bei einem Treffer wird die zugehörige Aktion ausgeführt. +5. Kein Treffer / Aktion `NONE` / `ALLOW` → der Block tut nichts, der Anruf läuft normal weiter. + +Die Aktionen nutzen die eingebauten STARFACE-Blöcke (`Hangup`, `Busy`, `CallPhonenumber2`, +`Answer` + `PlaybackResourceFile2`), die per Reflection im selben Anruf-Kontext aufgerufen +werden – dadurch bleibt der Block über STARFACE-Versionen hinweg robust. + +## Aktionen + +| Aktion | Wirkung | +|------------|---------------------------------------------------------------------| +| `HANGUP` | Anruf sofort auflegen (Anrufer wird getrennt) | +| `BUSY` | Besetztzeichen senden | +| `REDIRECT` | Anruf auf eine andere Nummer/Nebenstelle umleiten (Ziel erforderlich)| +| `ANNOUNCE` | Anruf annehmen, Ansage abspielen, dann auflegen | +| `ALLOW` | Ausnahme – Anruf trotz Treffer durchstellen (für Whitelist-Fälle) | +| `NONE` | (nur anonym) nichts tun, Anruf normal durchstellen | + +## Kompatibilität + +- STARFACE 8.x, 9.x, 10.x (Java 21) + +## Konfigurationsfelder (Eingabe) + +| Feld | Typ | Beschreibung | Default | +|------|-----|--------------|---------| +| `blacklistEntries` | STRING (mehrzeilig) | Nummern + optionale Aktion/Ziel, eine pro Zeile | | +| `defaultAction` | STRING (Auswahl) | Aktion für Einträge ohne eigene Aktion | `HANGUP` | +| `defaultRedirectTarget` | STRING | Ziel für REDIRECT-Einträge ohne eigenes Ziel | | +| `announcementFile` | STRING | Name der Sound-Ressource für `ANNOUNCE` | | +| `anonymousAction` | STRING (Auswahl) | Behandlung anonymer Anrufe | `NONE` | +| `anonymousRedirectTarget` | STRING | Ziel bei `REDIRECT` für anonyme Anrufe | | +| `countryCode` | STRING | Eigene Ländervorwahl (Deutschland = 49) | `49` | +| `prefixMatch` | BOOLEAN | Bereichssperre: Eintrag sperrt auch beginnende Nummern | `true` | + +## Ausgabe-Variablen + +| Feld | Typ | Beschreibung | +|------|-----|--------------| +| `caller` | STRING | Erkannte Anrufernummer (international normalisiert) | +| `isAnonymous` | BOOLEAN | Rufnummer unterdrückt? | +| `blocked` | BOOLEAN | Wurde der Anruf behandelt (geblockt/umgeleitet)? | +| `actionTaken` | STRING | Tatsächlich ausgeführte Aktion | +| `statusMessage` | STRING | Status-Meldung | + +## Format der Blacklist-Einträge + +Eine Nummer pro Zeile (Komma geht auch). Pro Zeile optional eine eigene Aktion und ein Ziel: + +``` +nummer;AKTION;ziel +``` + +Beispiele: + +``` +# Kommentarzeile, wird ignoriert ++49301234567 # nutzt die Standard-Aktion +0190;BUSY # dieser (Bereich) immer besetzt +004989;REDIRECT;100 # auf Nebenstelle 100 umleiten ++4972112345;ALLOW # Ausnahme: trotz Bereichssperre durchstellen +0900;ANNOUNCE # Ansage abspielen und auflegen +``` + +**Reihenfolge:** Der erste passende Eintrag gewinnt. `ALLOW`-Ausnahmen daher **vor** +breite Bereichssperren setzen. + +## Nummern-Vergleich + +Alle Nummern werden vor dem Vergleich in eine einheitliche internationale Ziffernform +gebracht (`countryCode` = 49): + +| Eingabe | normalisiert | +|---------|--------------| +| `+49 721 12345` | `4972112345` | +| `0049 721 12345` | `4972112345` | +| `0721 12345` | `4972112345` | +| `100` (intern) | `100` | + +Bei `prefixMatch = true` sperrt ein Eintrag auch alle Nummern, die mit ihm beginnen +(z.B. `0190` → gesamter `0190`-Bereich). Zum Schutz vor versehentlichem Über-Sperren +greift die Bereichssperre erst ab 3 Ziffern; kürzere Einträge werden nur exakt verglichen. + +## Build + +```bash +./build-block.sh +``` + +Die STARFACE-API-JARs werden in `libs/starface/` erwartet. Sind sie dort nicht vorhanden, +nutzt das Script automatisch die JARs des `mail2fax`-Moduls +(`../../mail2fax/v8-9-10/libs/starface/`). Ergebnis: `BlacklistBlock.class`. + +Es werden **keine** zusätzlichen Bibliotheken benötigt (kein JavaMail o.ä.). + +## Installation im Module Designer + +Siehe **[INSTALLATION.md](INSTALLATION.md)** für die Schritt-für-Schritt-Anleitung. + diff --git a/v8-9-10/build-block.sh b/v8-9-10/build-block.sh new file mode 100755 index 0000000..6efa6f6 --- /dev/null +++ b/v8-9-10/build-block.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# Kompiliert den BlacklistBlock für den STARFACE Module Designer +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "========================================" +echo " BlacklistBlock Kompilierung" +echo "========================================" +echo "" + +# STARFACE JARs finden. +# Standard: ./libs/starface - Fallback: das mail2fax-Modul (gleiches STARFACE-API). +LIB_DIR="${STARFACE_LIBS:-libs/starface}" +if [ ! "$(ls -A "$LIB_DIR" 2>/dev/null)" ]; then + ALT="../../mail2fax/v8-9-10/libs/starface" + if [ "$(ls -A "$ALT" 2>/dev/null)" ]; then + echo "Keine JARs in $LIB_DIR - verwende $ALT" + LIB_DIR="$ALT" + fi +fi + +CLASSPATH="" +for jar in "$LIB_DIR"/*.jar; do + if [ -f "$jar" ]; then + CLASSPATH="$CLASSPATH:$jar" + fi +done +CLASSPATH="${CLASSPATH:1}" + +if [ -z "$CLASSPATH" ]; then + echo "FEHLER: Keine STARFACE JARs gefunden." + echo "Lege die STARFACE-API-JARs in $LIB_DIR ab oder setze STARFACE_LIBS=." + echo "(Die JARs liegen bereits im mail2fax-Modul unter v8-9-10/libs/starface/)" + exit 1 +fi + +echo "Klassenpfad: $LIB_DIR" +echo "Kompiliere BlacklistBlock.java..." +javac -source 21 -target 21 \ + -cp "$CLASSPATH" \ + -proc:none \ + BlacklistBlock.java + +if [ -f "BlacklistBlock.class" ]; then + echo "" + echo "========================================" + echo " Erfolgreich!" + echo "========================================" + echo "" + ls -lh BlacklistBlock.class + echo "" + echo "Nächste Schritte:" + echo "1. STARFACE Admin -> Module -> Module Designer -> Neues Modul" + echo "2. Unter 'Ressourcen' BlacklistBlock.class hochladen" + echo " (optional: Ansage-WAV-Datei für die Aktion ANNOUNCE)" + echo "3. Eine Funktion 'Blacklist' anlegen und den Block hinzufügen" + echo "4. GUI-Felder anlegen und mit den @InputVar-Feldern verknüpfen" + echo "5. Modultyp 'Call-Processing', Aktivierung 'bei allen eingehenden Anrufen'" + echo "Details siehe INSTALLATION.md" + echo "" +else + echo "FEHLER: Kompilierung fehlgeschlagen" + exit 1 +fi diff --git a/v8-9-10/libs/starface/log4j-api-2.23.1.jar b/v8-9-10/libs/starface/log4j-api-2.23.1.jar new file mode 100644 index 0000000..0e8e3f5 Binary files /dev/null and b/v8-9-10/libs/starface/log4j-api-2.23.1.jar differ diff --git a/v8-9-10/libs/starface/log4j-core-2.23.1.jar b/v8-9-10/libs/starface/log4j-core-2.23.1.jar new file mode 100644 index 0000000..4a5d553 Binary files /dev/null and b/v8-9-10/libs/starface/log4j-core-2.23.1.jar differ diff --git a/v8-9-10/libs/starface/starface-classes.jar b/v8-9-10/libs/starface/starface-classes.jar new file mode 100644 index 0000000..99208be Binary files /dev/null and b/v8-9-10/libs/starface/starface-classes.jar differ diff --git a/v8-9-10/libs/starface/starface-commons-9.0.3.4.jar b/v8-9-10/libs/starface/starface-commons-9.0.3.4.jar new file mode 100644 index 0000000..de20433 Binary files /dev/null and b/v8-9-10/libs/starface/starface-commons-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-db-9.0.3.4.jar b/v8-9-10/libs/starface/starface-db-9.0.3.4.jar new file mode 100644 index 0000000..2aa3b01 Binary files /dev/null and b/v8-9-10/libs/starface/starface-db-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-db-converter-9.0.3.4.jar b/v8-9-10/libs/starface/starface-db-converter-9.0.3.4.jar new file mode 100644 index 0000000..995d6b5 Binary files /dev/null and b/v8-9-10/libs/starface/starface-db-converter-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-domain-objects-9.0.3.4.jar b/v8-9-10/libs/starface/starface-domain-objects-9.0.3.4.jar new file mode 100644 index 0000000..82eca86 Binary files /dev/null and b/v8-9-10/libs/starface/starface-domain-objects-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-ng-9.0.3.4.jar b/v8-9-10/libs/starface/starface-ng-9.0.3.4.jar new file mode 100644 index 0000000..85fa0dd Binary files /dev/null and b/v8-9-10/libs/starface/starface-ng-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-ng-api-9.0.3.4.jar b/v8-9-10/libs/starface/starface-ng-api-9.0.3.4.jar new file mode 100644 index 0000000..95d004b Binary files /dev/null and b/v8-9-10/libs/starface/starface-ng-api-9.0.3.4.jar differ diff --git a/v8-9-10/libs/starface/starface-rpc-9.0.3.4.jar b/v8-9-10/libs/starface/starface-rpc-9.0.3.4.jar new file mode 100644 index 0000000..dc9eca0 Binary files /dev/null and b/v8-9-10/libs/starface/starface-rpc-9.0.3.4.jar differ