The phones in group desk ring together. The first one that answers is connected. If nobody answers, your fail recording plays and the call hangs up. The group list does not ring by itself. The dial command does. Your server reads the group id and the timeout from your table.
1. Records in DotSIP
- Under SIP phones, users 201, 202, and 203, enabled, each with a password.
- Under Groups, id desk, members those three users.
- Put nobody.wav in data/media. Mono PCM16, 8000, 16000, or 24000 Hz.
- Under SIP accounts, the trunk that receives the call. Set its inbound route to route-tools, or leave that field empty so the call uses route-tools.
- route-tools URL http://127.0.0.1:9099/route.
2. Your database
CREATE TABLE ring (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL);
INSERT INTO ring(id, group_id, timeout_ms, fail_media) VALUES ('desk', 'desk', 25000, 'nobody.wav');
3. The server
<?php
$db = new PDO("sqlite:" . __DIR__ . "/operator.sqlite");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS ring (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO ring(id, group_id, timeout_ms, fail_media) VALUES ('desk', 'desk', 25000, 'nobody.wav')");
$body = json_decode(file_get_contents("php://input"), true);
header("Content-Type: application/json");
if (!is_array($body) || (isset($body["format"]) ? $body["format"] : "") !== "dotsip.route") {
http_response_code(400);
echo json_encode(array("error" => "bad request"));
return;
}
$row = $db->query("SELECT group_id, timeout_ms, fail_media FROM ring WHERE id = 'desk'")->fetch(PDO::FETCH_ASSOC);
echo json_encode(array(
"format" => "dotsip.route-result",
"format_version" => 2,
"commands" => array(
array("do" => "answer"),
array(
"do" => "dial",
"group" => $row["group_id"],
"mode" => "parallel",
"timeout_ms" => (int) $row["timeout_ms"],
"on_fail" => array(
array("do" => "play", "media" => $row["fail_media"], "barge_in" => false),
array("do" => "hangup"),
),
),
),
));
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
con = sqlite3.connect("operator.sqlite", check_same_thread=False)
con.execute("CREATE TABLE IF NOT EXISTS ring (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)")
con.execute("INSERT OR IGNORE INTO ring(id, group_id, timeout_ms, fail_media) VALUES ('desk', 'desk', 25000, 'nobody.wav')")
con.commit()
@app.post("/route")
def route():
body = request.get_json(silent=True) or {}
if body.get("format") != "dotsip.route":
return jsonify({"error": "bad request"}), 400
group_id, timeout_ms, fail_media = con.execute("SELECT group_id, timeout_ms, fail_media FROM ring WHERE id = ?", ("desk",)).fetchone()
return jsonify({"format": "dotsip.route-result", "format_version": 2, "commands": [
{"do": "answer"},
{"do": "dial", "group": group_id, "mode": "parallel", "timeout_ms": timeout_ms, "on_fail": [
{"do": "play", "media": fail_media, "barge_in": False},
{"do": "hangup"},
]},
]})
const http = require("http");
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("operator.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS ring (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)");
db.exec("INSERT OR IGNORE INTO ring(id, group_id, timeout_ms, fail_media) VALUES ('desk', 'desk', 25000, 'nobody.wav')");
const rowOf = db.prepare("SELECT group_id, timeout_ms, fail_media FROM ring WHERE id = ?");
http.createServer((req, res) => {
let raw = "";
req.on("data", (c) => { raw += c; });
req.on("end", () => {
let body = {};
try { body = JSON.parse(raw || "{}"); } catch (e) { body = {}; }
if (body.format !== "dotsip.route") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "bad request" }));
return;
}
const row = rowOf.get("desk");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ format: "dotsip.route-result", format_version: 2, commands: [
{ do: "answer" },
{ do: "dial", group: row.group_id, mode: "parallel", timeout_ms: row.timeout_ms, on_fail: [
{ do: "play", media: row.fail_media, barge_in: false },
{ do: "hangup" }
]}
]}));
});
}).listen(9099);
mode parallel rings the members together, at most 8, and keeps the first that answers. Empty mode is sequence, one idle extension at a time. This sample has no callback_url, so a miss only runs on_fail: nobody.wav, then hangup. The call stays up until that plan ends. If you add callback_url, a miss first posts dotsip.transfer-result with status no_answer, failed, or busy. A waited plan from that POST replaces on_fail. A connected parallel call posts dotsip.dial with status answered only when callback_url is set. You can send phones instead of group. number dials one outside number and does not use the group.