Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 26 additions & 18 deletions chatplan.xml
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<include>
<context name="public">
<extension name="ten-digit">
<condition field="to" expression="^(\d{10}.*)$">
<action application="set" data="final_delivery=true" />
<action application="lua" data="app.lua webtexting outbound" />
</condition>
</extension>
<extension name="unreg">
<condition field="${sofia_contact(profile/${to})}"
expression="error\/user_not_registered">
<action application="set" data="final_delivery=true" />
</condition>
</extension>
<extension name="other">
<condition field="to" expression="^(.*)$">
<action application="set" data="final_delivery=true" />
<!-- <action application="send"/> -->
</condition>
</extension>
<!-- Inbound delivery from carrier: PHP sets inbound=true (Patch 5) -->
<extension name="inbound_messaging">
<condition field="inbound" expression="^true$"/>
<condition field="to_user" expression="^\d{1,5}$" break="on-true">
<action application="set" data="sip_profile=internal-5065-5066" inline="true"/>
<action application="info" data="DEBUG" inline="true"/>
<action application="send" data="sip" inline="true"/>
</condition>
</extension>

<!-- Drop IMDN delivery notifications — Linphone sends these as SIP MESSAGEs
to confirm receipt. They should not route to the carrier API. -->
<extension name="imdn_filter">
<condition field="type" expression="^message/imdn">
<action application="info" data="IMDN notification dropped" inline="true"/>
</condition>
</extension>

<!-- All outbound: PSTN, short extensions, short codes -->
<extension name="outbound">
<condition field="to" expression="^(.+)$">
<action application="set" data="extension_uuid=${user_data(${from_user}@${from_host} var extension_uuid)}" inline="true"/>
<action application="set" data="final_delivery=true" inline="true"/>
<action application="lua" data="app.lua webtexting outbound" inline="true"/>
</condition>
</extension>
</context>
</include>
56 changes: 35 additions & 21 deletions outbound-hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,45 @@
// }

$event = json_decode(file_get_contents('php://input'));
// if (!$event) {
// error_log("failed to parse request body: ".$postbody);
// http_response_code(400);
// die();
// }
if (!$event) {
error_log("outbound-hook: failed to parse request body");
http_response_code(400);
die();
}

$domain_name = $event->{'from_host'};
$from = $event->from;
$to = $event->to;
$contentType = $event->contentType;
$body = urldecode($event->body);
$dedupeID = random_bytes(16);
$message_uuid = urldecode($event->id);
// ── Shared: domain is always from_host ──
$domain_name = $event->from_host;

// ── Normalize fields based on payload source ──
// Web UI sends extensionUUID (camelCase); FS event has extension_uuid (set by chatplan user_data)
if (isset($event->extensionUUID)) {
// Web UI payload (Ian's webtexting)
$to = $event->to;
$contentType = $event->contentType;
$body = urldecode($event->body);
$dedupeID = urldecode($event->id);
$extensionUUID = $event->extensionUUID;
$sql = "SELECT webtexting_destinations.phone_number, v_domains.domain_uuid FROM webtexting_destinations, v_domains, v_extensions WHERE v_domains.domain_name = :domain_name AND v_domains.domain_uuid = v_extensions.domain_uuid AND v_extensions.extension_uuid = :extensionUUID AND webtexting_destinations.extension_uuid = v_extensions.extension_uuid";
$parameters['domain_name'] = $domain_name;
$parameters['extensionUUID'] = $extensionUUID;
$db = new database;
$destination = $db->select($sql, $parameters, 'row');
if (!$destination) {
error_log("dropping outbound message from user with no configured destination: ".$extension."@".$domain_name);
die();
}
} else {
// FreeSWITCH event payload (from chatplan Lua via mod_curl)
// extension_uuid set by chatplan: ${user_data(${from_user}@${from_host} var extension_uuid)}
$to = $event->to_user;
$contentType = isset($event->type) ? $event->type : 'text/plain';
$body = isset($event->_body) ? urldecode($event->_body) : '';
$dedupeID = isset($event->{'sip_h_X-Message-ID'}) ? $event->{'sip_h_X-Message-ID'} : uuid();
$extensionUUID = $event->extension_uuid;
}

// ── Shared: look up phone number + domain UUID from extensionUUID (original query) ──
$sql = "SELECT webtexting_destinations.phone_number, v_domains.domain_uuid FROM webtexting_destinations, v_domains, v_extensions WHERE v_domains.domain_name = :domain_name AND v_domains.domain_uuid = v_extensions.domain_uuid AND v_extensions.extension_uuid = :extensionUUID AND webtexting_destinations.extension_uuid = v_extensions.extension_uuid";
$parameters['domain_name'] = $domain_name;
$parameters['extensionUUID'] = $extensionUUID;
$db = new database;
$destination = $db->select($sql, $parameters, 'row');
if (!$destination) {
error_log("outbound-hook: no configured destination for ".$extensionUUID."@".$domain_name);
http_response_code(404);
die();
}
unset($parameters);
$from = $destination['phone_number'];
$domainUUID = $destination['domain_uuid'];
Expand Down
92 changes: 53 additions & 39 deletions src/Messages.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ private static function _incoming(LocalNumber $destination, string $from, string
Messages::_sendWebPush($destination->domainUUID, $destination->extensionUUID, $from, $to, $bodyStr, $groupUUID);
}

// deliver via SSE or SIP or don't deliver
//_sendEvent("message", $bodyStr);
//Messages::_sendSIP($destination->domainName, $destination->extension, $from, $to, $bodyStr, $contentType, $messageUUID, $groupUUID);
// deliver via SIP
Messages::_sendSIP($destination->domainName, $destination->extension, $from, $to, $bodyStr, $contentType, $messageUUID, $groupUUID, null, true);
Comment thread
dunkeltron marked this conversation as resolved.
}

public static function OutgoingSMS(string $extensionUUID, string $domainUUID, string $from, string $to, string $body, string $messageUUID)
Expand Down Expand Up @@ -372,49 +371,64 @@ private static function _sendWebPush(string $domainUUID, string $extensionUUID,
unset($parameters);
}
}

private static function _sendSIP(string $domainName, string $extension, string $from, string $to, string $body, string $contentType, ?string $messageUUID, ?string $groupUUID = null, ?string $originalTo = null)
private static function _sendSIP(string $domainName, string $extension, string $from, string $to, string $body, string $contentType, ?string $dedupeID, ?string $groupUUID=null, ?string $originalTo=null, bool $inbound=false)
{
$SIPProfiles = array("websocket"); // TODO: make this list configurable
$toAddress = $extension . "@" . $domainName;
$fromAddress = $from . "@" . $domainName;

foreach ($SIPProfiles as $SIPProfile) {
$eventHeaders = array(
"Event-Subclass" => "SMS::SEND_MESSAGE",
"proto" => "sip",
"dest_proto" => "sip",
"from" => "sip:" . $from,
"from_user" => $from,
"from_host" => $domainName,
"from_full" => "sip:" . $fromAddress,
"sip_profile" => $SIPProfile,
"to" => $toAddress,
"to_user" => $extension,
"to_host" => $domainName,
"subject" => "SIMPLE MESSAGE", // is this required? what is it? fusionpbx's sms app does this
"type" => $contentType,
"hint" => "the hint", // is this required? what is it? fusionpbx's sms app does this
"replying" => "true", // what is this?
"DP_MATCH" => $toAddress, // what is this?
"sip_h_X-Message-ID" => $messageUUID,
"Content-Length" => strlen($body),
);
$toAddress = $extension."@".$domainName;
$fromAddress = $from."@".$domainName;

$baseHeaders = array(
"Event-Subclass" => "SMS::SEND_MESSAGE",
"proto" => "sip",
"from" => "sip:".$from,
"from_user" => $from,
"from_host" => $domainName,
"from_full" => "sip:".$fromAddress,
"to" => $toAddress,
"to_user" => $extension,
"to_host" => $domainName,
"subject" => "SIMPLE MESSAGE",
"type" => $contentType,
"hint" => "the hint",
"DP_MATCH" => $toAddress,
"sip_h_X-Message-ID" => $dedupeID,
"Content-Length" => strlen($body),
);

if ($groupUUID != null) {
$eventHeaders['sip_h_X-Group-ID'] = $groupUUID;
}
if ($groupUUID != null) {
$baseHeaders['sip_h_X-Group-ID'] = $groupUUID;
}
if ($originalTo != null) {
$baseHeaders['sip_h_X-Original-To'] = $originalTo;
}

if ($originalTo != null) {
$eventHeaders['sip_h_X-Original-To'] = $originalTo;
}
$destinations = array(
array(
"dest_proto" => "sip",
"sip_profile" => "websocket",
"replying" => "true",
),
);

// Only deliver to SIP device for inbound traffic (carrier → local user).
// For outbound, the carrier API handles all delivery (including returning
// the message if the destination is a local extension).
if ($inbound) {
$destinations[] = array(
"dest_proto" => "GLOBAL_SMS",
"context" => "public",
"inbound" => "true",
);
}

foreach ($destinations as $overrides) {
$eventHeaders = array_merge($baseHeaders, $overrides);

$cmd = "sendevent CUSTOM\n";
foreach ($eventHeaders as $k => $v) {
$cmd .= "$k: " . $v . "\n";
$cmd .= "$k: $v\n";
}

$cmd .= "\n" . $body;
$cmd .= "\n".$body;

event_socket_request_cmd($cmd);
}
Expand Down
Loading