The caller hears a WAV in a loop until a phone in the group is idle. Your server does not poll the phones. It only tells DotSIP the group id, the file name, and the timeout, all read from your table. DotSIP connects the first idle extension. On timeout it runs on_fail.
A group in the admin is a named list of extension users. The list alone does not ring anyone. The queue command does.
1. Records in DotSIP
- Under SIP phones, users 201 and 202, enabled, each with a password.
- Under Groups, id reception, label Reception. Members 201 and 202. all is built in and is not this group.
- Put queue.wav and queue-end.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 queues (
id TEXT PRIMARY KEY,
group_id TEXT NOT NULL,
media TEXT NOT NULL,
timeout_ms INTEGER NOT NULL,
fail_media TEXT NOT NULL
);
INSERT INTO queues(id, group_id, media, timeout_ms, fail_media) VALUES
('main', 'reception', 'queue.wav', 60000, 'queue-end.wav');
The group_id must be the group id in DotSIP. The members are the phones in that group, capped at 8 by the queue command.
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 queues (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, media TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO queues(id, group_id, media, timeout_ms, fail_media) VALUES ('main', 'reception', 'queue.wav', 60000, 'queue-end.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, media, timeout_ms, fail_media FROM queues WHERE id = 'main'")->fetch(PDO::FETCH_ASSOC);
echo json_encode(array(
"format" => "dotsip.route-result",
"format_version" => 2,
"commands" => array(
array("do" => "answer"),
array(
"do" => "queue",
"group" => $row["group_id"],
"media" => $row["media"],
"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 queues (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, media TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)")
con.execute("INSERT OR IGNORE INTO queues(id, group_id, media, timeout_ms, fail_media) VALUES ('main', 'reception', 'queue.wav', 60000, 'queue-end.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, media, timeout_ms, fail_media = con.execute("SELECT group_id, media, timeout_ms, fail_media FROM queues WHERE id = ?", ("main",)).fetchone()
return jsonify({"format": "dotsip.route-result", "format_version": 2, "commands": [
{"do": "answer"},
{"do": "queue", "group": group_id, "media": media, "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 queues (id TEXT PRIMARY KEY, group_id TEXT NOT NULL, media TEXT NOT NULL, timeout_ms INTEGER NOT NULL, fail_media TEXT NOT NULL)");
db.exec("INSERT OR IGNORE INTO queues(id, group_id, media, timeout_ms, fail_media) VALUES ('main', 'reception', 'queue.wav', 60000, 'queue-end.wav')");
const rowOf = db.prepare("SELECT group_id, media, timeout_ms, fail_media FROM queues 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("main");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ format: "dotsip.route-result", format_version: 2, commands: [
{ do: "answer" },
{ do: "queue", group: row.group_id, media: row.media, timeout_ms: row.timeout_ms, on_fail: [
{ do: "play", media: row.fail_media, barge_in: false },
{ do: "hangup" }
]}
]}));
});
}).listen(9099);
DotSIP polls line status and transfers to the first idle member of reception. queue.wav loops while it waits. After 60 seconds, queue-end.wav plays and the call hangs up. Default timeout if you omit it is 120000, from 1000 to 300000. You can send phones instead of group.