Skip to content

Commit 666f967

Browse files
authored
added conference DTMF function (#23)
1 parent 3356dab commit 666f967

7 files changed

Lines changed: 236 additions & 7 deletions

File tree

‎applications/crossbar/doc/conference.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,18 @@ Playing a media file to everyone in a conference:
349349

350350
`{MEDIA_ID}` can be a pre-uploaded media ID or a URL to fetch media from.
351351

352+
### Sending DTMF to a conference
353+
354+
Sending DTMF tones to everyone in a conference:
355+
356+
```json
357+
{"data":{
358+
"action":"dtmf",
359+
"data":{"digits":"{DTMF DIGITS [0-9,A-D,*,#]}"}
360+
}
361+
}
362+
```
363+
352364
## Perform an action on participants
353365

354366
> PUT /v2/accounts/{ACCOUNT_ID}/conferences/{CONFERENCE_ID}/participants
@@ -440,6 +452,7 @@ curl -v -X PUT \
440452
`undeaf` | Start sending conference audio to the participant
441453
`kick` | Kick the participant from the conference
442454
`play` | Play media to a single participant
455+
`dtmf` | Send DTMF tones to a single participant
443456

444457
### Playing media to a conference
445458

@@ -455,6 +468,18 @@ Playing a media file to everyone in a conference:
455468

456469
`{MEDIA_ID}` can be a pare-uploaded media ID or a URL to fetch media from.
457470

471+
### Sending DTMF tones to a conference participant
472+
473+
Sending DTMF to a participant in a conference:
474+
475+
```json
476+
{"data":{
477+
"action":"dtmf",
478+
"data":{"digits":"{DTMF DIGITS [0-9,A-D,*,#]}"}
479+
}
480+
}
481+
```
482+
458483
### List of conferences example
459484

460485
```json

‎applications/crossbar/priv/api/swagger.json‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11087,6 +11087,47 @@
1108711087
],
1108811088
"type": "object"
1108911089
},
11090+
"kapi.conference.dtmf": {
11091+
"description": "AMQP API for conference.dtmf",
11092+
"properties": {
11093+
"Application-Name": {
11094+
"enum": [
11095+
"dtmf"
11096+
],
11097+
"type": "string"
11098+
},
11099+
"Conference-ID": {
11100+
"type": "string"
11101+
},
11102+
"Digits": {
11103+
"type": [
11104+
"number",
11105+
"string"
11106+
]
11107+
},
11108+
"Event-Category": {
11109+
"enum": [
11110+
"conference"
11111+
],
11112+
"type": "string"
11113+
},
11114+
"Event-Name": {
11115+
"enum": [
11116+
"command"
11117+
],
11118+
"type": "string"
11119+
},
11120+
"Participant-ID": {
11121+
"type": "string"
11122+
}
11123+
},
11124+
"required": [
11125+
"Application-Name",
11126+
"Conference-ID",
11127+
"Digits"
11128+
],
11129+
"type": "object"
11130+
},
1109011131
"kapi.conference.event": {
1109111132
"description": "AMQP API for conference.event",
1109211133
"properties": {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"_id": "kapi.conference.dtmf",
4+
"description": "AMQP API for conference.dtmf",
5+
"properties": {
6+
"Application-Name": {
7+
"enum": [
8+
"dtmf"
9+
],
10+
"type": "string"
11+
},
12+
"Conference-ID": {
13+
"type": "string"
14+
},
15+
"Digits": {
16+
"type": [
17+
"number",
18+
"string"
19+
]
20+
},
21+
"Event-Category": {
22+
"enum": [
23+
"conference"
24+
],
25+
"type": "string"
26+
},
27+
"Event-Name": {
28+
"enum": [
29+
"command"
30+
],
31+
"type": "string"
32+
},
33+
"Participant-ID": {
34+
"type": "string"
35+
}
36+
},
37+
"required": [
38+
"Application-Name",
39+
"Conference-ID",
40+
"Digits"
41+
],
42+
"type": "object"
43+
}

‎applications/crossbar/src/modules/cb_conferences.erl‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%%%-----------------------------------------------------------------------------
2-
%%% @copyright (C) 2011-2022, 2600Hz
2+
%%% @copyright (C) 2011-2026, 2600Hz
33
%%% @doc Conferences module
44
%%% Handle client requests for conference documents
55
%%%
@@ -9,10 +9,10 @@
99
%%% /v2/accounts/{AccountId}/conferences/{ConferenceID}/participants
1010
%%% /v2/accounts/{AccountId}/conferences/{ConferenceID}/participants/{ParticipantId}
1111
%%%
12-
%%%
1312
%%% @author Karl Anderson
1413
%%% @author James Aimonetti
1514
%%% @author Roman Galeev
15+
%%% @author Ruel Tmeizeh for Umojo, Inc.
1616
%%% @end
1717
%%%-----------------------------------------------------------------------------
1818
-module(cb_conferences).
@@ -46,6 +46,7 @@
4646
-define(KICK, <<"kick">>).
4747
-define(RELATE, <<"relate">>).
4848
-define(PLAY, <<"play">>).
49+
-define(DTMF, <<"dtmf">>).
4950

5051
-define(PUT_ACTION, <<"action">>).
5152

@@ -222,6 +223,9 @@ put(Context, ConferenceId, ?PARTICIPANTS, ParticipantId) ->
222223
participant_action(Context, ConferenceId, ParticipantId, ?PLAY) ->
223224
play(Context, ConferenceId, ParticipantId, cb_context:req_value(Context, <<"data">>));
224225

226+
participant_action(Context, ConferenceId, ParticipantId, ?DTMF) ->
227+
dtmf(Context, ConferenceId, ParticipantId, cb_context:req_value(Context, <<"data">>));
228+
225229
participant_action(Context, ConferenceId, ParticipantId, Action) ->
226230
perform_participant_action(conference(ConferenceId), Action, kz_term:to_integer(ParticipantId)),
227231
crossbar_util:response_202(<<"ok">>, Context).
@@ -374,6 +378,8 @@ handle_conference_action(Context, ConferenceId, <<"unlock">>) ->
374378
crossbar_util:response_202(<<"ok">>, Context);
375379
handle_conference_action(Context, ConferenceId, ?PLAY) ->
376380
play(Context, ConferenceId, cb_context:req_value(Context, <<"data">>));
381+
handle_conference_action(Context, ConferenceId, ?DTMF) ->
382+
dtmf(Context, ConferenceId, cb_context:req_value(Context, <<"data">>));
377383
handle_conference_action(Context, ConferenceId, <<"dial">>) ->
378384
dial(Context, ConferenceId, cb_context:req_value(Context, <<"data">>));
379385
handle_conference_action(Context, ConferenceId, Action) ->
@@ -442,6 +448,43 @@ media_id_required(Context) ->
442448
,Context
443449
).
444450

451+
%% send DTMF to all participants in conference (ParticipantId undefined)
452+
-spec dtmf(cb_context:context(), path_token(), kz_term:api_object()) -> cb_context:context().
453+
dtmf(Context, _ConferenceId, 'undefined') ->
454+
data_required(Context, <<"dtmf">>);
455+
dtmf(Context, ConferenceId, Data) ->
456+
dtmf(Context, ConferenceId, 'undefined', Data).
457+
458+
-spec dtmf(cb_context:context(), path_token(), pos_integer(), kz_term:api_object()) -> cb_context:context().
459+
dtmf(Context, _ConferenceId, _ParticipantId, 'undefined') ->
460+
data_required(Context, <<"dtmf">>);
461+
dtmf(Context, ConferenceId, ParticipantId, Data) ->
462+
ParticipantMsg =
463+
case kz_term:to_api_binary(ParticipantId) of
464+
'undefined' -> <<>>;
465+
Participant -> <<" participant ", Participant/binary>>
466+
end,
467+
case dtmf_digits(kz_json:get_value(<<"digits">>, Data)) of
468+
<<>> -> digits_required(Context);
469+
DTMF ->
470+
lager:info("sending DTMF ~s to conference ~s~s", [DTMF, ConferenceId, ParticipantMsg]),
471+
kapps_conference_command:dtmf(DTMF, ParticipantId, conference(ConferenceId)),
472+
crossbar_util:response_202(<<"ok">>, Context)
473+
end.
474+
475+
-spec dtmf_digits(any()) -> binary().
476+
dtmf_digits('undefined') -> <<>>;
477+
dtmf_digits(<<>>) -> <<>>;
478+
dtmf_digits(Digits) -> re:replace(kz_term:to_binary(Digits), "[^a-dA-D0-9*#]", "", [global, {return, binary}]).
479+
480+
-spec digits_required(cb_context:context()) -> cb_context:context().
481+
digits_required(Context) ->
482+
cb_context:add_validation_error([<<"data">>, <<"digits">>]
483+
,<<"required">>
484+
,kz_json:from_list([{<<"message">>, <<"action 'dtmf' requires digits [0123456789*#ABCD] to be specified">>}])
485+
,Context
486+
).
487+
445488
-spec dial(cb_context:context(), path_token(), kz_term:api_object()) -> cb_context:context().
446489
dial(Context, _ConferenceId, 'undefined') ->
447490
data_required(Context, <<"dial">>);
@@ -779,6 +822,8 @@ handle_participants_action(Context, ConferenceId, Action=?KICK) ->
779822
handle_participants_action(Context, ConferenceId, Action, fun kz_term:always_true/1);
780823
handle_participants_action(Context, ConferenceId, Action=?PLAY) ->
781824
handle_conference_action(Context, ConferenceId, Action);
825+
handle_participants_action(Context, ConferenceId, Action=?DTMF) ->
826+
handle_conference_action(Context, ConferenceId, Action);
782827
handle_participants_action(Context, ConferenceId, ?RELATE) ->
783828
OnSuccess = fun(C) -> handle_participants_relate(C, ConferenceId) end,
784829
RelateData = cb_context:req_value(Context, <<"data">>, kz_json:new()),

‎applications/ecallmgr/src/ecallmgr_conference_command.erl‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
%%%-----------------------------------------------------------------------------
2-
%%% @copyright (C) 2011-2022, 2600Hz
2+
%%% @copyright (C) 2011-2026, 2600Hz
33
%%% @doc Execute conference commands
4+
%%%
5+
%%% @author 2600Hz
6+
%%% @author Ruel Tmeizeh for Umojo, Inc.
47
%%% @end
58
%%%-----------------------------------------------------------------------------
69
-module(ecallmgr_conference_command).
@@ -140,6 +143,22 @@ get_conf_command(<<"stop_play">>, _Focus, _ConferenceId, JObj) ->
140143
{<<"stop">>, Args}
141144
end;
142145

146+
get_conf_command(<<"dtmf">>, _Focus, _ConferenceId, JObj) ->
147+
case kapi_conference:dtmf_v(JObj) of
148+
'false' ->
149+
{'error', <<"conference dtmf failed to execute as JObj did not validate.">>};
150+
'true' ->
151+
DigitsRaw = kz_term:to_binary(kz_json:get_value(<<"Digits">>, JObj)),
152+
Digits = re:replace(DigitsRaw, "[^a-dA-D0-9*#]", "", [global, {return, binary}]),
153+
Participant =
154+
case kz_json:get_ne_binary_value(<<"Participant-ID">>, JObj) of
155+
'undefined' -> <<"all">>;
156+
P -> P
157+
end,
158+
Args = list_to_binary([Participant, " ", Digits]),
159+
{<<"dtmf">>, Args}
160+
end;
161+
143162
get_conf_command(Say, _Focus, _ConferenceId, JObj)
144163
when Say =:= <<"say">>;
145164
Say =:= <<"tts">> ->

‎core/kazoo_amqp/src/api/kapi_conference.erl‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
%%%-----------------------------------------------------------------------------
2-
%%% @copyright (C) 2012-2022, 2600Hz
2+
%%% @copyright (C) 2012-2026, 2600Hz
33
%%% @doc
44
%%% @author Karl Anderson
5+
%%% @author Ruel Tmeizeh for Umojo, Inc.
56
%%% @end
67
%%%-----------------------------------------------------------------------------
78
-module(kapi_conference).
@@ -23,6 +24,7 @@
2324
-export([mute_participant/1, mute_participant_v/1]).
2425
-export([play/1, play_v/1
2526
,tones/1, tones_v/1
27+
,dtmf/1, dtmf_v/1
2628
,say/1, say_v/1
2729
,tts/1, tts_v/1
2830
]).
@@ -304,6 +306,17 @@
304306
,{<<"Media-Name">>, fun is_binary/1}
305307
]).
306308

309+
%% Conference DTMF
310+
-define(DTMF_HEADERS, [<<"Application-Name">>, <<"Conference-ID">>, <<"Digits">>]).
311+
-define(OPTIONAL_DTMF_HEADERS, [<<"Participant-ID">>]).
312+
-define(DTMF_VALUES, [{<<"Event-Category">>, <<"conference">>}
313+
,{<<"Event-Name">>, <<"command">>}
314+
,{<<"Application-Name">>, <<"dtmf">>}
315+
]).
316+
-define(DTMF_TYPES, [{<<"Conference-ID">>, fun is_binary/1}
317+
,{<<"Digits">>, fun is_valid_dtmf/1}
318+
]).
319+
307320
%% Conference Record
308321
-define(RECORD_HEADERS, [<<"Application-Name">>, <<"Conference-ID">>, <<"Media-Name">>]).
309322
-define(OPTIONAL_RECORD_HEADERS, [<<"Call-ID">>]).
@@ -562,6 +575,7 @@
562575
,{<<"participant_volume_out">>, ?PARTICIPANT_VOLUME_OUT_VALUES, fun participant_volume_out/1}
563576
,{<<"dial">>, ?DIAL_VALUES, fun dial/1}
564577
,{<<"dial_resp">>, ?DIAL_RESP_VALUES, fun dial_resp/1}
578+
,{<<"dtmf">>, ?DTMF_VALUES, fun dtmf/1}
565579
,{<<"tones">>, ?CONF_TONES_REQ_VALUES, fun tones/1}
566580
,{<<"say">>, ?CONF_SAY_REQ_VALUES, fun say/1}
567581
,{<<"tts">>, ?CONF_SAY_REQ_VALUES, fun tts/1}
@@ -912,6 +926,32 @@ play_macro_req_v(Prop) when is_list(Prop) ->
912926
kz_api:validate(Prop, ?CONF_PLAY_MACRO_REQ_HEADERS, ?CONF_PLAY_MACRO_REQ_VALUES, ?CONF_PLAY_MACRO_REQ_TYPES);
913927
play_macro_req_v(JObj) -> play_macro_req_v(kz_json:to_proplist(JObj)).
914928

929+
%%------------------------------------------------------------------------------
930+
%% @doc Takes {@link kz_term:proplist()}, creates JSON string or error.
931+
%% @end
932+
%%------------------------------------------------------------------------------
933+
-spec dtmf(kz_term:api_terms()) -> {'ok', iolist()} | {'error', string()}.
934+
dtmf(Prop) when is_list(Prop) ->
935+
case dtmf_v(Prop) of
936+
'true' -> kz_api:build_message(Prop, ?DTMF_HEADERS, ?OPTIONAL_DTMF_HEADERS);
937+
'false' -> {'error', "Proplist failed validation for dtmf"}
938+
end;
939+
dtmf(JObj) -> dtmf(kz_json:to_proplist(JObj)).
940+
941+
-spec dtmf_v(kz_term:api_terms()) -> boolean().
942+
dtmf_v(Prop) when is_list(Prop) ->
943+
kz_api:validate(Prop, ?DTMF_HEADERS, ?DTMF_VALUES, ?DTMF_TYPES);
944+
dtmf_v(JObj) -> dtmf_v(kz_json:to_proplist(JObj)).
945+
946+
-spec is_valid_dtmf(binary() | number()) -> boolean().
947+
is_valid_dtmf(Digits) when is_binary(Digits) ->
948+
case re:replace(Digits, "[^a-dA-D0-9*#]", "", [global, {return, binary}]) of
949+
<<>> -> 'false';
950+
_DTMF -> 'true'
951+
end;
952+
is_valid_dtmf(Digits) when is_number(Digits) -> is_valid_dtmf(kz_term:to_binary(Digits));
953+
is_valid_dtmf(_) -> 'false'.
954+
915955
%%------------------------------------------------------------------------------
916956
%% @doc Takes {@link kz_term:proplist()}, creates JSON string or error.
917957
%% @end

0 commit comments

Comments
 (0)