├── .github
└── FUNDING.yml
├── .gitignore
├── Backup.php
├── LICENSE
├── README.md
├── Restore.php
├── Smsconnector.class.php
├── adaptor
└── Smsconnector.class.php
├── agi-bin
└── sipsmsconn.agi
├── assets
├── README.md
├── js
│ └── smsconnector.js
└── less
│ └── smsconnector.less
├── i18n
├── es_ES
│ └── LC_MESSAGES
│ │ ├── smsconnector.mo
│ │ └── smsconnector.po
└── smsconnector.pot
├── module.xml
├── page.smsconnector.php
├── providers
├── Sample_provider-name.php
├── provider-Bandwidth.php
├── provider-Bulkvs.php
├── provider-Commio.php
├── provider-Flowroute.php
├── provider-Signalwire.php
├── provider-Sinch.php
├── provider-Siptrunk.php
├── provider-Skyetel.php
├── provider-Telnyx.php
├── provider-Twilio.php
├── provider-Voipms.php
├── provider-Voxtelesys.php
└── providerBase.php
├── public
├── media.php
└── provider.php
└── views
├── page.main.php
├── rnav.php
├── settings.php
├── view.number.form.php
├── view.number.grid.php
└── view.userman.user.php
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | polar: # Replace with a single Polar username
14 | custom: "https://simon.tel/tips"
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | node/logs/install.log
3 | assets/less/cache
4 | .vscode
5 | module.sig
6 |
--------------------------------------------------------------------------------
/Backup.php:
--------------------------------------------------------------------------------
1 | addDependency('sms');
9 | $configs = [
10 | 'kvstore' => $this->dumpKVStore(),
11 | 'tables' => $this->dumpTables(),
12 | ];
13 | $this->addConfigs($configs);
14 | }
15 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## SMS Connector
2 | A third-party SMS connector module for FreePBX 16 and 17
3 |
4 | ### Overview
5 |
6 | FreePBX offers SMS and MMS functionality through UCP (User Control Panel) and the Sangoma Connect softphones.
7 | This integrates tightly with Sangoma number services (SIPStation and VoIP Innovations) but until now there have been
8 | no open source modules allowing integration with third-party providers. The aim of this module is to provide
9 | a generic, expandable connector, with new providers added as contributed by the community.
10 |
11 | ### Features
12 |
13 | * Send and receive SMS and MMS through UCP and Sangoma Connect
14 | * Mix-and-match: if you have numbers on multiple providers you are not limited to just one
15 |
16 | ### Providers
17 |
18 | * Bandwidth: Messaging API v2 (https://dev.bandwidth.com/apis/messaging/) (Doesn't currently support status callbacks)
19 | * Bulk Solutions (Bulkvs): API v1 (https://portal.bulkvs.com/api/v1.0/documentation)
20 | * Commio/Thinq: (https://apidocs.thinq.com/#bac2ace6-7777-47d8-931e-495b62f01799)
21 | * Flowroute: Messaging API v2.2, webhook v2.1 (https://developer.flowroute.com/api/messages/v2.2/)
22 | * Sinch: SMS API (https://developers.sinch.com/docs/sms)
23 | * SignalWire: Messaging API (https://developer.signalwire.com/guides/messaging-overview/)
24 | * Siptrunk: Messaging API
25 | * Skyetel: SMS and MMS API (https://support.skyetel.com/hc/en-us/articles/360056299914-SMS-MMS-API)
26 | * Telnyx: Messaging API v2 (https://developers.telnyx.com/docs/api/v2/messaging)
27 | * Twilio: Messaging API version 2010-04-01 (https://www.twilio.com/docs/sms)
28 | * Voip.ms: SMS and MMS API (https://voip.ms/m/apidocs.php)
29 | * Voxtelesys: Messaging API v1 (https://smsapi.voxtelesys.com)
30 |
31 | ### Installation
32 |
33 | * `fwconsole ma downloadinstall https://github.com/simontelephonics/smsconnector/releases/download/v16.0.17.2/smsconnector-16.0.17.2.tar.gz`
34 | * `fwconsole r`
35 |
36 | ### Configuration
37 |
38 | #### General requirements
39 |
40 | For inbound SMS/MMS and outbound MMS to work, you will need an HTTPS path inbound to your PBX from your provider(s). This means:
41 | * Import or generate a TLS certificate in Certificate Manager
42 | * Enable it on the web server using Sysadmin Pro or by manually configuring Apache
43 | * Allow your provider(s) webhook addresses through the FreePBX Firewall and/or your network firewall
44 | * Set the public DNS name in the `AMPWEBADDRESS` setting: Advanced Settings -> FreePBX Web Address
45 |
46 | Sending of SMS/MMS requires verification and registration performed through your provider and is outside of the scope of this
47 | module or document.
48 |
49 | #### Provider Settings
50 |
51 | Once set up with your provider, generate or locate the required credentials, typically an API key and secret,
52 | and enter these into the SMS Connector -> Provider Settings screen.
53 |
54 | In the provider portal, set the webhook URL for inbound SMS/MMS in the format shown on the Provider Settings screen ("Webhook Provider").
55 |
56 | #### Adding Numbers
57 |
58 | Enter a number (DID), pick the user(s) to which the DID should be assigned, and select the provider for that number.
59 |
60 | ### Possible Improvements
61 |
62 | * Configuration within User Management (dummy screen right now)
63 | * Automatic addition of FreePBX Firewall rules when module is installed and removal when module is uninstalled
64 |
--------------------------------------------------------------------------------
/Restore.php:
--------------------------------------------------------------------------------
1 | getConfigs();
9 | $this->importKVStore($configs['kvstore']);
10 | $this->importTables($configs['tables']);
11 | }
12 |
13 | public function processLegacy($pdo, $data, $tables, $unknownTables)
14 | {
15 | $this->restoreLegacyDatabase($pdo);
16 | }
17 | }
--------------------------------------------------------------------------------
/Smsconnector.class.php:
--------------------------------------------------------------------------------
1 | 'smsconnector_relations',
17 | );
18 | protected $tablesSms = array(
19 | 'routing' => 'sms_routing',
20 | 'dids' => 'sms_dids',
21 | );
22 |
23 | public function __construct($freepbx = null)
24 | {
25 | if ($freepbx == null) {
26 | throw new \Exception("Not given a FreePBX Object");
27 | }
28 | $this->FreePBX = $freepbx;
29 | $this->Database = $freepbx->Database;
30 | $this->Userman = $freepbx->Userman;
31 |
32 | $this->loadProviders();
33 | }
34 |
35 | /**
36 | * smsAdaptor loaded by SMS module hook (loadAdapter)
37 | *
38 | * @param string $adaptor Adaptor name
39 | * @return Smsconnector adaptor object
40 | */
41 | public function smsAdaptor($adaptor)
42 | {
43 | if(!class_exists('\FreePBX\modules\Sms\Adaptor\Smsconnector')) {
44 | include __DIR__.'/adaptor/Smsconnector.class.php';
45 | }
46 | return new \FreePBX\modules\Sms\Adaptor\Smsconnector($adaptor);
47 | }
48 |
49 | /**
50 | * Installer run on fwconsole ma install
51 | *
52 | * @return void
53 | */
54 | public function install()
55 | {
56 | outn(_("Creating link to public folder..."));
57 | $link_public = sprintf("%s/smsconn", $this->FreePBX->Config->get("AMPWEBROOT"));
58 | $link_public_module = sprintf("%s/admin/modules/smsconnector/public", $this->FreePBX->Config->get("AMPWEBROOT"));
59 | if (! file_exists($link_public))
60 | {
61 | symlink($link_public_module, $link_public);
62 | out(_("Done"));
63 | }
64 | else
65 | {
66 | out(_('Skip: the path already exists!'));
67 | }
68 | // if daemon got installed in 16.0.13, remove it
69 | if (class_exists('FreePBX\modules\Pm2') && $this->FreePBX->Pm2->getStatus("smsconnector-sipsms"))
70 | {
71 | $this->FreePBX->Pm2->delete("smsconnector-sipsms");
72 | }
73 | }
74 |
75 | /**
76 | * Uninstaller run on fwconsole ma uninstall
77 | *
78 | * @return void
79 | */
80 | public function uninstall()
81 | {
82 | outn(_("Removing public folder link..."));
83 | $link_public = sprintf("%s/smsconn", $this->FreePBX->Config->get("AMPWEBROOT"));
84 | if(file_exists($link_public))
85 | {
86 | if(is_link($link_public))
87 | {
88 | unlink($link_public);
89 | if( ! file_exists($link_public))
90 | {
91 | out(_("Done"));
92 | }
93 | else
94 | {
95 | out(_("Error: the path still exists!"));
96 | }
97 | }
98 | else
99 | {
100 | out(_("Skip: the path is not a symbolic link!"));
101 | }
102 | }
103 | }
104 |
105 | /**
106 | * Processes form submission and pre-page actions.
107 | *
108 | * @param string $page Display name
109 | * @return void
110 | */
111 | public function doConfigPageInit($page)
112 | {
113 | /** getReq provided by FreePBX_Helpers see https://wiki.freepbx.org/x/0YGUAQ */
114 | $action = $this->getReq('action', '');
115 | $providers = $this->getReq('providers');
116 |
117 | switch ($action)
118 | {
119 | case 'setproviders':
120 | return $this->updateProviders($providers);
121 | break;
122 | }
123 | }
124 |
125 | public function getRightNav($request)
126 | {
127 | switch($request['view'])
128 | {
129 | case 'settings':
130 | return load_view(dirname(__FILE__).'/views/rnav.php', array());
131 | break;
132 | default:
133 | //No show Nav
134 | }
135 | }
136 |
137 | /**
138 | * Adds buttons to the bottom of pages per set conditions
139 | *
140 | * @param array $request $_REQUEST
141 | * @return void
142 | */
143 | public function getActionBar($request)
144 | {
145 | if ('smsconnector' == $request['display'])
146 | {
147 | if (!isset($_GET['view']))
148 | {
149 | return [];
150 | }
151 | $buttons = [
152 | 'reset' => [
153 | 'name' => 'reset',
154 | 'id' => 'reset',
155 | 'value' => _("Reset")
156 | ],
157 | 'submit' => [
158 | 'name' => 'submit',
159 | 'id' => 'submit',
160 | 'value' => _("Submit")
161 | ]
162 | ];
163 | return $buttons;
164 | }
165 | }
166 |
167 | /**
168 | * Returns bool permissions for AJAX commands
169 | * https://wiki.freepbx.org/x/XoIzAQ
170 | * @param string $command The ajax command
171 | * @param array $setting ajax settings for this command typically untouched
172 | * @return bool
173 | */
174 | public function ajaxRequest($req, &$setting)
175 | {
176 | // ** Allow remote consultation with Postman **
177 | // ********************************************
178 | // $setting['authenticate'] = false;
179 | // $setting['allowremote'] = true;
180 | // return true;
181 | // ********************************************
182 | switch($req)
183 | {
184 | case "get_selects":
185 | case "numbers_list":
186 | case "numbers_get":
187 | case "numbers_update":
188 | case "numbers_delete":
189 | return true;
190 | break;
191 |
192 | default:
193 | return false;
194 | }
195 | return false;
196 | }
197 |
198 | /**
199 | * Handle Ajax request
200 | */
201 | public function ajaxHandler()
202 | {
203 | $command = $this->getReq("command", "");
204 | $data_return = false;
205 |
206 | switch ($command)
207 | {
208 | case 'get_selects':
209 | $data['users'] = array();
210 | $data['providers'] = array();
211 | foreach($this->Userman->getAllUsers() as $user)
212 | {
213 | $data['users'][$user['id']] = empty($user['displayname']) ? $user['username'] : sprintf('%s (%s)', $user['displayname'], $user['username']);
214 | }
215 | foreach ($this->getAvailableProviders() as $provider => $info)
216 | {
217 | $data['providers'][$info['nameraw']] = $info['name'];
218 | }
219 | $data_return = array("status" => true, 'data' => $data);
220 | break;
221 |
222 | case 'numbers_list':
223 | $data_return = $this->getList();
224 | break;
225 |
226 | case 'numbers_get':
227 | $id = $this->getReq("id", null);
228 | if (empty($id))
229 | {
230 | $data_return = array("status" => false, "message" => _("ID is missing!"));
231 | }
232 | else if (! $this->isExistDIDByID($id))
233 | {
234 | $data_return = array("status" => false, "message" => _("ID does not exist!"));
235 | }
236 | else
237 | {
238 | $dataId = $this->getNumber($id);
239 | $data_return = array("status" => true, "data" => $dataId);
240 | }
241 | break;
242 |
243 | case 'numbers_update':
244 | $getdata = $this->getReq("data", array());
245 | $id = $getdata['id'];
246 | $did = $getdata['didNumber'];
247 | $uids = $getdata['uidsNumber'];
248 | $name = $getdata['providerNumber'];
249 |
250 | $uids = explode(",", $uids);
251 | try
252 | {
253 | if ($getdata['type'] == 'edit')
254 | {
255 | $this->updateNumber($uids, $did, $name);
256 | $data_return = array("status" => true, "message" => _("Number updated successfully"));
257 | }
258 | else
259 | {
260 | $this->addNumber($uids, $did, $name);
261 | $data_return = array("status" => true, "message" => _("Number created successfully"));
262 | }
263 | }
264 | catch (\Exception $e)
265 | {
266 | $data_return = array("status" => false, "message" => $e->getMessage());
267 | }
268 |
269 | break;
270 |
271 | case 'numbers_delete':
272 | $id = $this->getReq("id", null);
273 | if (empty($id))
274 | {
275 | $data_return = array("status" => false, "message" => _("ID is missing!"));
276 | }
277 | else if (! $this->isExistDIDByID($id))
278 | {
279 | $data_return = array("status" => false, "message" => _("ID does not exist!"));
280 | }
281 | else if ($this->deleteNumber($id))
282 | {
283 | $data_return = array("status" => true, "message" => _("Number delete successfully"));
284 | }
285 | else
286 | {
287 | $data_return = array("status" => false, "message" => _("Number delete failed!"));
288 | }
289 | break;
290 |
291 | default:
292 | $data_return = array("status" => false, "message" => _("Command not found!"), "command" => $command);
293 | }
294 | return $data_return;
295 | }
296 |
297 | /**
298 | * getProviderSettings
299 | * @return array returns an associative array
300 | */
301 | public function getProviderSettings()
302 | {
303 | return array('providers' => $this->getAll('provider'));
304 | }
305 |
306 | /**
307 | * getAvailableProviders
308 | * @return array list of providers that are configured and available for use
309 | */
310 | public function getAvailableProviders()
311 | {
312 | $retlist = array();
313 | $list = $this->getProvider("");
314 | foreach ($list as $key => $value)
315 | {
316 | if ($value['class']->isAvailable())
317 | {
318 | $retlist[$key] = $value;
319 | }
320 | }
321 | return $retlist;
322 | }
323 |
324 | /**
325 | * getList gets a list of numbers and their associations
326 | * @return array
327 | */
328 | public function getList()
329 | {
330 | $sql = sprintf('SELECT r.id, rt.didid, r.providerid AS name, GROUP_CONCAT(DISTINCT rt.uid) AS users, rt.did FROM %s AS rt ' .
331 | 'INNER JOIN %s as r ON rt.didid = r.didid ' .
332 | 'WHERE rt.adaptor = "%s" ' .
333 | 'GROUP BY r.id, rt.didid, r.providerid, rt.did', $this->tablesSms['routing'], $this->tables['relations'], self::adapterName);
334 |
335 | $data = $this->Database->query($sql)->fetchAll(\PDO::FETCH_NAMED);
336 | foreach ($data as $key => &$value)
337 | {
338 | $value['users'] = $this->getInfoUserByID($value['users'], true);
339 | }
340 | return $data;
341 | }
342 |
343 | /**
344 | * getNumber Gets an individual item by smsconnector_relations.ID
345 | * @param int $id Item ID
346 | * @return array Returns an associative array with id, subject and body.
347 | */
348 | public function getNumber($id)
349 | {
350 | $data_return = null;
351 | if ($this->isExistDIDByID($id))
352 | {
353 | $sql = sprintf('SELECT r.id, rt.didid, r.providerid AS name, GROUP_CONCAT(DISTINCT rt.uid) AS users, rt.did FROM %s as rt ' .
354 | 'INNER JOIN %s as r ON rt.didid = r.didid ' .
355 | 'WHERE rt.adaptor = "%s" AND rt.didid = :id ' .
356 | 'GROUP BY r.id, rt.didid, r.providerid, rt.did', $this->tablesSms['routing'], $this->tables['relations'], self::adapterName);
357 | $stmt = $this->Database->prepare($sql);
358 | $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
359 | $stmt->execute();
360 | $row = $stmt->fetchObject();
361 | $data_return = [
362 | 'id' => $row->id,
363 | 'didid' => $row->didid,
364 | 'name' => $row->name,
365 | 'users' => $this->getInfoUserByID($row->users, true),
366 | 'did' => $row->did,
367 | ];
368 | }
369 | return $data_return;
370 | }
371 |
372 | /**
373 | * getInfoUserByID We obtain the information of the users with the userman module.
374 | * @param mixed $uid UID or array of UIDs of the users that we want to obtain information.
375 | * @param bool $needExplode True if we need to exploit the uids, False (default) if nothing needs to be done.
376 | * @param string $explodeChar The character the used for the explode (default ',').
377 | * @param array $info_return The array of the information that we wish to obtain (default userman and displayname).
378 | * @return array Array of the information the users.
379 | */
380 | private function getInfoUserByID($uid, $needExplode = false, $explodeChar = ",", $info_return = null)
381 | {
382 | if ($needExplode && ! is_array($needExplode))
383 | {
384 | $uid = explode($explodeChar, $uid);
385 | }
386 | if (! is_array($uid))
387 | {
388 | $uid = array($uid);
389 | }
390 |
391 | $data_return = array();
392 | if (is_null($info_return))
393 | {
394 | $info_return = array('username', 'displayname');
395 | }
396 | foreach ($uid as $userid)
397 | {
398 | $user_info = $this->Userman->getUserByID($userid);
399 | $data_user = array('uid' => $userid);
400 |
401 | foreach ($info_return as $option)
402 | {
403 | $data_user[$option] = $user_info[$option];
404 | }
405 | $data_return[$userid] = $data_user;
406 | }
407 | return $data_return;
408 | }
409 |
410 | /* Helper functions */
411 | public function getUsersByDid($did)
412 | {
413 | return $this->FreePBX->Sms->getAssignedUsers($did);
414 | }
415 |
416 | public function getDidsByUser($uid)
417 | {
418 | return $this->FreePBX->Sms->getDIDs($uid);
419 | }
420 |
421 | public function getUidByDefaultExtension($extension)
422 | {
423 | return $this->Userman->getUserByDefaultExtension($extension)['id'];
424 | }
425 |
426 | public function getSipDefaultDidByUid($uid)
427 | {
428 | return $this->Userman->getModuleSettingByID($uid, 'smsconnector', 'sipsmsdefaultdid', true, false);
429 | }
430 |
431 | /**
432 | * getSIPMessageDeviceByUserID
433 | * @param int $uid UID of the users that we want to obtain information.
434 | * @return mixed extension number or NULL
435 | */
436 | public function getSIPMessageDeviceByUserID($uid)
437 | {
438 | if ($this->Userman->getModuleSettingByID($uid, 'smsconnector', 'sipsmsenabled', false, false))
439 | {
440 | $user = $this->Userman->getUserByID($uid);
441 | $extension = $user['default_extension'];
442 | $device = $this->FreePBX->Core->getDevice($extension);
443 |
444 | // only select PJSIP devices that have been set with the appropriate message_context
445 | if ($device['tech'] == 'pjsip' && $device['message_context'] == 'smsconnector-messages')
446 | {
447 | return $extension;
448 | }
449 | }
450 | return NULL;
451 | }
452 |
453 | /**
454 | * processOutboundSip Used by AGI script to send SMS via connector
455 | * @param string $to To (destination)
456 | * @param string $from Extension that is sending the SMS
457 | * @param string $messageBody SMS body
458 | * @return bool
459 | */
460 | public function processOutboundSip($to, $from, $messageBody)
461 | {
462 | freepbx_log(FPBX_LOG_INFO, sprintf(_("Processing SIP SMS from %s to %s"), $from, $to), true);
463 | $did = $actualTo = NULL;
464 | $allowedToSend = false;
465 | $retval = true;
466 | if (preg_match('/^\+?(\d+)\+\+?(\d+)$/', $to, $matches))
467 | {
468 | $did = $matches[1];
469 | $actualTo = $matches[2];
470 | }
471 | elseif (preg_match('/^\+?(\d+)$/', $to, $matches))
472 | {
473 | $actualTo = $matches[1];
474 | }
475 |
476 | $uid = $this->getUidByDefaultExtension($from);
477 | if ($this->Userman->getModuleSettingByID($uid, 'smsconnector', 'sipsmsenabled', false, false))
478 | {
479 | if ($did)
480 | {
481 | if (in_array($did, $this->getDidsByUser($uid))) { $allowedToSend = true; }
482 | }
483 | else
484 | {
485 | if ($defaultDid = $this->getSipDefaultDidByUid($uid))
486 | {
487 | $did = $defaultDid;
488 | $allowedToSend = true;
489 | }
490 | }
491 | }
492 |
493 | if ($allowedToSend && $actualTo)
494 | {
495 | $formattedTo = (preg_match('/^[2-9][0-9]{2}[2-9][0-9]{6}$/', $actualTo)) ? '1'.$actualTo : $actualTo; // format NANP
496 |
497 | $adaptor = $this->FreePBX->Sms->getAdaptor($did);
498 | if(is_object($adaptor)) {
499 | $o = $adaptor->sendMessage($formattedTo, $did, '', $messageBody);
500 | if(! $o['status']) {
501 | freepbx_log(FPBX_LOG_INFO, sprintf(_("Outbound message failed: %s"), $o['message']), true);
502 | $retval = false;
503 | }
504 | } else {
505 | freepbx_log(FPBX_LOG_INFO, sprintf(_("No adaptor found for DID %s"), $did), true);
506 | $retval = false;
507 | }
508 | }
509 | else
510 | {
511 | freepbx_log(FPBX_LOG_INFO, sprintf(_("%s tried to send SMS from %s but is not allowed!"), $from, $did), true);
512 | $retval = false;
513 | }
514 |
515 | return $retval;
516 | }
517 |
518 | /**
519 | * inboundHookForSip Hooked by AdaptorBase getMessage on inbound SMS. Relays SMS to SIP devices if enabled.
520 | * @param string $to To (destination)
521 | * @param string $from caller ID
522 | * @param string $message SMS body
523 | */
524 | public function inboundHookForSip($id, $to, $from, $cnam, $message, $time, $adaptor, $emid, $threadid, $didid)
525 | {
526 | // lookup users for DID
527 | $uids = $this->getUsersByDid($to);
528 |
529 | foreach ($uids as $uid)
530 | {
531 | // get device that can receive SMS
532 | $device = $this->getSIPMessageDeviceByUserID($uid);
533 |
534 | if ($device)
535 | {
536 | if ($to != $this->getSipDefaultDidByUid($uid))
537 | {
538 | // format the caller ID to include the DID, which will allow replying via non-default DID
539 | $sipFrom = sprintf("%s+%s", $to, $from);
540 | }
541 | else
542 | {
543 | $sipFrom = $from;
544 | }
545 |
546 | // get contacts
547 | $result = $this->FreePBX->astman->send_request('Getvar', array('Variable' => "PJSIP_DIAL_CONTACTS($device)"));
548 | $contacts = array();
549 | if (!empty($result['Value']))
550 | {
551 | $contacts = explode('&', $result['Value']);
552 | }
553 |
554 | if (!empty($contacts))
555 | {
556 | foreach ($contacts as $contact) // message all registered
557 | {
558 | $sipTo = sprintf("pjsip:%s", substr($contact, 6)); // replace "PJSIP/" with "pjsip:"
559 | $result = $this->FreePBX->astman->MessageSend($sipTo, $sipFrom, $message);
560 | if ($result['Response'] == 'Error')
561 | {
562 | freepbx_log(FPBX_LOG_INFO, sprintf(_("Error sending message to %s: %s"), $contact, $result['Message']));
563 | }
564 | }
565 | }
566 | else // no contacts registered - send email if enabled
567 | {
568 | if ($this->Userman->getModuleSettingByID($uid, 'smsconnector', 'sipsmsemailoffline', false, false)) {
569 | // if no email address defined for user, does nothing
570 | //TODO: Generate body from template.
571 | $body = sprintf(_("While offline, you received an SMS from %s to %s:\n\n%s"), $from, $to, $message);
572 | //TODO: Allow setting the subject message
573 | $subject = _('SMS received while offline');
574 | $this->Userman->sendEmail($uid, $subject, $body);
575 | }
576 | }
577 | }
578 | }
579 | }
580 |
581 | /**
582 | * addNumber Add a number
583 | * @param int $uid userman user id
584 | * @param string $did DID
585 | * @param string $name name of the SMS provider
586 | */
587 | public function addNumber($uid, $did, $name, $checkExists = true)
588 | {
589 | if (! is_array($uid))
590 | {
591 | $uid = array($uid);
592 | }
593 |
594 | if (preg_match('/^[2-9]\d{2}[2-9]\d{6}$/', $did)) // ten digit NANP, make it 11-digit
595 | {
596 | $did = '1'.$did;
597 | }
598 |
599 | if ( ($name == "") || ($did == "") || (empty($uid)))
600 | {
601 | throw new \Exception(_('Necessary data is missing!'));
602 | }
603 | else if (($checkExists == true) && ($this->isExistDID($did)))
604 | {
605 | throw new \Exception(_('The DID already exists!'));
606 | }
607 |
608 | $this->FreePBX->Sms->addDIDRouting($did, $uid, self::adapterName);
609 |
610 | $sql = sprintf("SELECT id FROM %s WHERE did = :did", $this->tablesSms['dids']);
611 | $sth = $this->Database->prepare($sql);
612 | $sth->execute(array(':did' => $did));
613 | $didid = $sth->fetchColumn();
614 |
615 | if (! empty($didid))
616 | {
617 | $sql = sprintf('INSERT INTO %s (didid, providerid) VALUES (:didid, :provider) ON DUPLICATE KEY UPDATE providerid = :provider', $this->tables['relations']);
618 | $stmt = $this->Database->prepare($sql);
619 | $stmt->bindParam(':didid', $didid, \PDO::PARAM_INT);
620 | $stmt->bindParam(':provider', $name, \PDO::PARAM_STR);
621 | $stmt->execute();
622 | return true;
623 | }
624 |
625 | return false;
626 | }
627 |
628 | /**
629 | * updateNumber Updates the given ID
630 | * @param int $uid userman user ID
631 | * @param string $did DID
632 | * @param string $name provider name
633 | * @return bool Returns true on success or false on failure
634 | */
635 | public function updateNumber($uid, $did, $name)
636 | {
637 | return $this->addNumber($uid, $did, $name, false);
638 | }
639 |
640 | /**
641 | * deleteNumber Deletes the given number by smsconnector_relations.didid
642 | * @param int $id ID
643 | * @return bool Returns true on success or false on failure
644 | */
645 | public function deleteNumber($id)
646 | {
647 | if ($this->isExistDIDByID($id))
648 | {
649 | $sql = sprintf('DELETE FROM %s WHERE didid = :id', $this->tables['relations']);
650 | $stmt = $this->Database->prepare($sql);
651 | $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
652 | $stmt->execute();
653 |
654 | $sql = sprintf('DELETE FROM %s WHERE didid = :id', $this->tablesSms['routing']);
655 | $stmt = $this->Database->prepare($sql);
656 | $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
657 | $stmt->execute();
658 |
659 | return true;
660 | }
661 | return false;
662 | }
663 |
664 | /**
665 | * isExistDIDByID Check if the ID exists using the ID or DIDID (table relations)
666 | * @param int $id ID
667 | * @param bool $useddidid True used column DIDID, False used column ID (defualt DIDID).
668 | * @return bool Returns true on existe of false if is not exist or not definded id.
669 | */
670 | public function isExistDIDByID($id, $useddidid = true)
671 | {
672 | if (trim($id) != "")
673 | {
674 | $sql = sprintf('SELECT COUNT(*) FROM %s WHERE %s = :id', $this->tables['relations'], ($useddidid ? 'didid' : 'id'));
675 | $stmt = $this->Database->prepare($sql);
676 | $stmt->bindParam(':id', $id, \PDO::PARAM_INT);
677 | $stmt->execute();
678 | if ($stmt->fetchColumn() > 0)
679 | {
680 | return true;
681 | }
682 | }
683 | return false;
684 | }
685 |
686 | /**
687 | * isExistDID Check if the Did exist
688 | * @param string $did Number DID
689 | * @return bool Returns true on exist and False if not exist.
690 | */
691 | public function isExistDID($did)
692 | {
693 | $data_raturn = false;
694 | if (trim($did) != "")
695 | {
696 | $sql = sprintf('SELECT COUNT(*) FROM %s as r INNER JOIN %s AS d ON r.didid = d.id WHERE d.did = :did', $this->tables['relations'], $this->tablesSms['dids']);
697 | $stmt = $this->Database->prepare($sql);
698 | $stmt->bindParam(':did', $did, \PDO::PARAM_STR);
699 | $stmt->execute();
700 | if ($stmt->fetchColumn() > 0)
701 | {
702 | $data_raturn = true;
703 | }
704 | }
705 | return $data_raturn;
706 | }
707 |
708 | /**
709 | * updateProviders
710 | * @param array hash of provider settings from form
711 | * @return bool success or failure
712 | */
713 | public function updateProviders($providers)
714 | {
715 | foreach ($providers as $provider => $creds)
716 | {
717 | $this->setProviderConfig($provider, $creds);
718 | }
719 | return true;
720 | }
721 |
722 | /**
723 | * getUsersWithDids
724 | * @return array of user IDs associated with SMS DIDs
725 | */
726 | public function getUsersWithDids()
727 | {
728 | $sql = sprintf('SELECT DISTINCT uid FROM %s', $this->tablesSms['routing']);
729 | return $this->Database->query($sql)->fetchAll(\PDO::FETCH_COLUMN, 0);
730 | }
731 |
732 | /**
733 | * This returns html to the main page
734 | *
735 | * @return string html
736 | */
737 | public function showPage($page, $params = array())
738 | {
739 | $request = $_REQUEST;
740 | $data = array(
741 | "smsconnector" => $this,
742 | 'request' => $request,
743 | 'page' => $page,
744 | );
745 | $data = array_merge($data, $params);
746 |
747 | switch ($page)
748 | {
749 | case 'main':
750 | $data_return = load_view(__DIR__ . '/views/page.main.php', $data);
751 | break;
752 |
753 | case 'grid':
754 | $data_return = load_view(__DIR__ . '/views/view.number.grid.php', $data);
755 | $data_return .= load_view(__DIR__ . '/views/view.number.form.php', $data);
756 | break;
757 |
758 | case 'settings':
759 | foreach ($this->listProviders() as $provider)
760 | {
761 | $data['settings'][$provider]['info'] = $this->getProvider($provider);
762 | // unset($data['settings'][$provider]['info']['class']);
763 | $data['settings'][$provider]['value'] = $this->getProviderConfig($provider);
764 | }
765 | $data_return = load_view(__DIR__ . '/views/settings.php', $data);
766 | break;
767 |
768 | case 'userman':
769 | $data['userman'] =& $this->Userman;
770 | $data_return = load_view(__DIR__ . '/views/view.userman.user.php', $data);
771 | break;
772 |
773 | default:
774 | $data_return = sprintf(_("Page Not Found (%s)!!!!"), $page);
775 | }
776 | return $data_return;
777 | }
778 |
779 | public function usermanShowPage()
780 | {
781 | $request = $_REQUEST;
782 | if(isset($request['action']))
783 | {
784 | switch($request['action'])
785 | {
786 | case 'adduser':
787 | case 'showuser':
788 | $sipSmsEnabled = null;
789 | $defaultDid = null;
790 | $emailOffline = null;
791 |
792 | if(isset($request['user']))
793 | {
794 | $user = $this->Userman->getUserByID($request['user']);
795 | $sipSmsEnabled = $this->Userman->getModuleSettingByID($user['id'],'smsconnector','sipsmsenabled',true);
796 | $defaultDid = $this->Userman->getModuleSettingByID($user['id'],'smsconnector','sipsmsdefaultdid',true);
797 | $emailOffline = $this->Userman->getModuleSettingById($user['id'],'smsconnector','sipsmsemailoffline',true);
798 | }
799 | return array(
800 | array(
801 | 'title' => _('SMS Connector'),
802 | 'rawname' => 'smsconnector',
803 | 'content' => $this->showPage('userman', array(
804 | 'error' => empty($error)?'':$error,
805 | 'sipsmsenabled' => $sipSmsEnabled,
806 | 'sipsmsdefaultdid' => $defaultDid,
807 | 'sipsmsemailoffline' => $emailOffline,
808 | 'dids' => !empty($request['user']) ? $this->getDidsByUser($request['user']) : array()
809 | )),
810 | )
811 | );
812 | break;
813 | }
814 | }
815 | }
816 |
817 | public function usermanAddUser($id, $display, $data)
818 | {
819 | $this->usermanUpdateUser($id, $display, $data);
820 | }
821 |
822 | public function usermanUpdateUser($id, $display, $data)
823 | {
824 | $post = $_POST;
825 | if($display == 'userman' && isset($post['type']) && $post['type'] == 'user')
826 | {
827 | if(isset($post['sipsmsenabled']))
828 | {
829 | if($post['sipsmsenabled'] == "true")
830 | {
831 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsenabled', true);
832 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsdefaultdid', !empty($post['sipsmsdefaultdid']) ? $post['sipsmsdefaultdid'] : null);
833 | if (!empty($post['sipsmsemailoffline']))
834 | {
835 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsemailoffline', ($post['sipsmsemailoffline'] == "true") ? true : false);
836 | }
837 | else
838 | {
839 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsemailoffline', null);
840 | }
841 | }
842 | elseif($post['sipsmsenabled'] == "false")
843 | {
844 | $this->Userman->setModuleSettingByID($id,'smsconnector','sipsmsenabled',false);
845 | }
846 | else
847 | {
848 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsenabled', null);
849 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsdefaultdid', null);
850 | $this->Userman->setModuleSettingByID($id, 'smsconnector', 'sipsmsemailoffline', null);
851 | }
852 | }
853 | }
854 | }
855 |
856 | public function usermanDelUser($id, $display, $data) {
857 | freepbx_log(FPBX_LOG_INFO, sprintf(_("SMS Connector received delete request for user id %s ; removing all DID assignments"), $id));
858 | $this->FreePBX->Sms->addUserRouting($id,array(),'Smsconnector'); // "add" with empty array is delete
859 | }
860 |
861 | private function loadProviders()
862 | {
863 | include_once dirname(__FILE__) . "/providers/providerBase.php";
864 | $this->providers = array();
865 | foreach (glob(dirname(__FILE__) . "/providers/provider-*.php") as $filename)
866 | {
867 | if (file_exists($filename))
868 | {
869 | include_once $filename;
870 |
871 | preg_match('/provider-(.*)\.php/i', $filename, $matches);
872 | $this_provider_name = $matches[1];
873 | $this_provider_name_full = sprintf("FreePBX\modules\Smsconnector\Provider\%s", $this_provider_name);
874 | $this_provider_name_lower = strtolower($this_provider_name);
875 |
876 | if(class_exists($this_provider_name_full))
877 | {
878 | $this_provider_class = new $this_provider_name_full();
879 |
880 | $this->providers[$this_provider_name_lower]['name'] = $this_provider_class->getName();
881 | $this->providers[$this_provider_name_lower]['nameraw'] = $this_provider_class->getNameRaw();
882 | $this->providers[$this_provider_name_lower]['configs'] = $this_provider_class->getConfigInfo();
883 | $this->providers[$this_provider_name_lower]['webhook'] = $this_provider_class->getWebHookUrl();
884 | $this->providers[$this_provider_name_lower]['class_full'] = $this_provider_name_full;
885 | $this->providers[$this_provider_name_lower]['class_name'] = $this_provider_name;
886 | $this->providers[$this_provider_name_lower]['class'] = $this_provider_class;
887 | }
888 | }
889 | }
890 | }
891 |
892 | public function listProviders()
893 | {
894 | return array_keys($this->providers);
895 | }
896 |
897 | public function getProvider($name)
898 | {
899 | $return_data = array();
900 | if (empty($name))
901 | {
902 | $return_data = $this->providers;
903 | }
904 | else
905 | {
906 | if (array_key_exists($name, $this->providers))
907 | {
908 | $return_data = $this->providers[$name];
909 | }
910 | }
911 | return $return_data;
912 | }
913 |
914 | public function getProviderConfigDefault($name)
915 | {
916 | $data_return = array();
917 | $info = $this->getProvider($name);
918 | foreach ($info['configs'] as $config => $options)
919 | {
920 | $data_return[$config] = isset($options['default']) ? $options['default'] : '';
921 | }
922 | return $data_return;
923 | }
924 |
925 | public function getProviderConfig($name)
926 | {
927 | $data_return = array();
928 |
929 | $default = $this->getProviderConfigDefault($name);
930 | $setting = $this->getConfig($name, 'provider');
931 |
932 | foreach ($default as $option => $value)
933 | {
934 | $data_return[$option] = isset($setting[$option]) ? $setting[$option] : $value;
935 | }
936 | return $data_return;
937 | }
938 |
939 | public function setProviderConfig($name, $config)
940 | {
941 | $this->setConfig($name, $config, 'provider');
942 | }
943 |
944 | public function myDialplanHooks()
945 | {
946 | return true;
947 | }
948 |
949 | public function doDialplanHook(&$ext, $engine, $priority)
950 | {
951 | foreach ($this->getUsersWithDids() as $uid)
952 | {
953 | if ($this->Userman->getModuleSettingByID($uid, 'smsconnector', 'sipsmsenabled', false, false))
954 | {
955 | $user = $this->Userman->getUserByID($uid);
956 | $extension = $user['default_extension'];
957 | $device = $this->FreePBX->Core->getDevice($extension);
958 | if ($device['tech'] == 'pjsip')
959 | {
960 | $de = $this->kvArrayifyDeviceValues($device);
961 | $de['message_context']['value'] = 'smsconnector-messages';
962 | $this->FreePBX->Core->delDevice($extension, true);
963 | $this->FreePBX->Core->addDevice($extension, 'pjsip', $de, true);
964 | }
965 | }
966 | }
967 | }
968 |
969 | private function kvArrayifyDeviceValues($values) { // stolen verbatim from Core module
970 | $response = array();
971 | $flag = 2;
972 | $ignoreTheseKeys = array('id', 'tech');
973 | foreach($values as $key => $value) {
974 | if (in_array($key, $ignoreTheseKeys)) {
975 | continue;
976 | }
977 |
978 | $response[$key] = array(
979 | 'value' => $value,
980 | 'flag' => $flag++
981 | );
982 | }
983 | return $response;
984 | }
985 | }
986 |
--------------------------------------------------------------------------------
/adaptor/Smsconnector.class.php:
--------------------------------------------------------------------------------
1 | getmessage());
28 | }
29 |
30 | // Look up provider info containing name and api credentials
31 | $provider = $this->lookUpProvider($from);
32 | $providerInfo = $this->FreePBX->Smsconnector->getProvider($provider);
33 | if (!empty($providerInfo))
34 | {
35 | if (! empty($providerInfo['class']))
36 | {
37 | $providerInfo['class']->sendMedia($retval['id'], $to, $from, $message);
38 | }
39 | }
40 | $retval['emid'] = $emid; // TODO: set this from API call result
41 | return $retval;
42 | }
43 |
44 | public function sendMessage($to,$from,$cnam,$message,$time=null,$adaptor=null,$emid=null,$chatId='')
45 | {
46 | $message ??= ''; // string is expected, so don't allow a null
47 |
48 | // Clean up 'to' number if we got one with a +. Only seems to apply to mobile app. UCP
49 | // and desktop apps format number before invoking this module.
50 | $to = ltrim($to, '+');
51 |
52 | if (preg_match('/^[2-9][0-9]{2}[2-9][0-9]{6}$/', $to)) // rewrite NANP for mobile app
53 | {
54 | $to = sprintf('1%s', $to);
55 | }
56 |
57 | // Store in database
58 | $retval = array();
59 | try
60 | {
61 | $retval['id'] = parent::sendMessage($to, $from, $cnam, $message, $time, 'Smsconnector', $emid, $chatId);
62 | $retval['status'] = true;
63 | }
64 | catch (\Exception $e)
65 | {
66 | throw new \Exception('Unable to store message: '.$e->getMessage());
67 | }
68 |
69 | // look up provider info containing name and api credentials
70 | $provider = $this->lookUpProvider($from);
71 |
72 | $providerInfo = $this->FreePBX->Smsconnector->getProvider($provider);
73 | if (!empty($providerInfo))
74 | {
75 | if (! empty($providerInfo['class']))
76 | {
77 | $providerInfo['class']->sendMessage($retval['id'], $to, $from, $message);
78 | }
79 | }
80 | $retval['emid'] = $emid; // TODO: set this from API call result
81 | return $retval;
82 | }
83 |
84 | public function getMessage($to,$from,$cnam,$message,$time=null,$adaptor=null,$emid=null)
85 | {
86 | return parent::getMessage($to, $from, $cnam, $message, $time, 'Smsconnector', $emid);
87 | }
88 |
89 | public function updateMessageByEMID($emid,$message,$adaptor=null)
90 | {
91 | return parent::updateMessageByEMID($emid, $message, 'Smsconnector');
92 | }
93 |
94 | /**
95 | * Looks up provider info given DID
96 | *
97 | * @param string $did
98 | * @return array hash containing provider details
99 | */
100 | private function lookUpProvider($did)
101 | {
102 | $sql = 'SELECT providerid FROM smsconnector_relations as r ' .
103 | 'INNER JOIN sms_dids AS d ON r.didid = d.id ' .
104 | 'WHERE d.did = :did';
105 |
106 | $stmt = $this->db->prepare($sql);
107 | $stmt->bindParam(':did', $did, \PDO::PARAM_STR);
108 | $stmt->execute();
109 | $row = $stmt->fetchObject();
110 |
111 | return $row->providerid;
112 | }
113 |
114 | public function dialPlanHooks(&$ext, $engine, $priority): void
115 | {
116 | if ($engine != "asterisk") { return; }
117 | $section = 'smsconnector-messages';
118 | $p = '_X.';
119 | $ext->add($section, $p, '', new \ext_NoOp('Processing outbound SIP SMS'));
120 | $ext->add($section, $p, '', new \ext_AGI('sipsmsconn.agi'));
121 | $ext->add($section, $p, '', new \ext_Hangup);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/agi-bin/sipsmsconn.agi:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | true);
6 | require_once '/etc/freepbx.conf';
7 |
8 | $smsconnector = \FreePBX::create()->Smsconnector();
9 | require_once 'phpagi.php';
10 |
11 | $AGI = new AGI();
12 | $messageTo = get_var($AGI, "MESSAGE(to)");
13 | $messageFrom = get_var($AGI, "MESSAGE(from)");
14 | $messageBody = get_var($AGI, "MESSAGE(body)");
15 |
16 | $matches = array();
17 | if (preg_match('/:(.*)@/', $messageFrom, $matches))
18 | {
19 | $from = $matches[1];
20 | }
21 | else return;
22 |
23 | if (preg_match('/:(.*)@/', $messageTo, $matches))
24 | {
25 | $to = $matches[1];
26 | }
27 | else return;
28 |
29 | $AGI->verbose(sprintf(_("SMS SIP connector got message from %s to %s"), $from, $to), 3);
30 | $success = $smsconnector->processOutboundSip($to, $from, $messageBody);
31 | if (!$success)
32 | {
33 | $AGI->verbose(_("SMS SIP connector failed to send message. Check freepbx.log for details"), 3);
34 | }
35 |
36 | exit;
37 |
38 | // helper functions
39 | function get_var($agi, $value)
40 | {
41 | $r = $agi->get_variable($value);
42 |
43 | if ($r['result'] == 1)
44 | {
45 | $result = $r['data'];
46 | return $result;
47 | }
48 | else return '';
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/assets/README.md:
--------------------------------------------------------------------------------
1 | assets/css
2 | ===========
3 |
4 | This is for module specific styling
5 |
6 | assets/js
7 | ==========
8 | This is for module specific Java script.
9 |
10 |
11 | Javascript Libraries
12 | ====================
13 |
14 | The following libraries are built in already and can be used without inclusion.
15 |
16 |
17 | - jquery-1.6.2.min.js
18 | - jquery-1.7.1.min.js
19 | - jquery-ui-1.8.16.min.js
20 | - jquery-ui-1.8.9.min.js
21 | - jquery.cookie.js
22 | - jquery.hotkeys.js
23 | - jquery.toggleval.3.0.js
24 |
--------------------------------------------------------------------------------
/assets/js/smsconnector.js:
--------------------------------------------------------------------------------
1 | //This format's the action column
2 | function linkFormat(value, row, idx){
3 | var html = '';
4 | html += ' ';
5 | html += '';
6 | return html;
7 | }
8 |
9 | function userFormat(value, row, idx)
10 | {
11 | var html = '';
12 | $.each(value, function(uid, user_info)
13 | {
14 | html += ''
15 | html += (user_info['displayname'] == "") ? user_info['username'] : sprintf("%s (%s)", user_info['username'], user_info['displayname']);
16 | html += '';
17 | });
18 | return html;
19 | }
20 |
21 | $(document).ready(function() {
22 | $(".webhook-copy").click(function() {
23 | var input = $(this).siblings("input");
24 | input.select();
25 | document.execCommand("copy");
26 | window.getSelection().removeAllRanges();
27 | fpbxToast(_("URL copied to clipboard"), '', 'success' );
28 | });
29 | });
30 |
31 |
32 | function updateInputSelects()
33 | {
34 | var status_return = true;
35 |
36 | $('#uidsNumber').empty();
37 | $('#available_users').empty();
38 | $('#selected_users').empty();
39 |
40 | $('#providerNumber').empty();
41 |
42 | $.ajax({
43 | type: "POST",
44 | url: window.FreePBX.ajaxurl,
45 | data: {
46 | module : 'smsconnector',
47 | command : 'get_selects',
48 | },
49 | async: false,
50 | success: function(response)
51 | {
52 | if (response.status)
53 | {
54 | $.each(response.data.users, function(user_id, user_display) {
55 | $("#available_users").append(sprintf('
%s',user_id, user_display));
56 | });
57 | $.each(response.data.providers, function(provider_id, provider_display) {
58 | $('#providerNumber').append($('