@webex/plugin-meetings 1.144.2 → 1.146.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,4 +73,39 @@ export default class MembersRequest extends StatelessWebexPlugin {
73
73
 
74
74
  return this.request(requestParams);
75
75
  }
76
+
77
+ /**
78
+ * Sends a request to the DTMF endpoint to send tones
79
+ * @param {Object} options
80
+ * @param {String} options.locusUrl
81
+ * @param {String} options.url device url SIP user
82
+ * @param {String} options.tones a string of one or more DTMF tones to send
83
+ * @param {String} options.memberId ID of PSTN user
84
+ * @returns {Promise}
85
+ */
86
+ sendDialPadKey(options) {
87
+ if (!options || !options.locusUrl || !options.memberId || !options.url || !options.tones && options.tones !== 0) {
88
+ throw new ParameterError('memberId must be defined, the associated locus url, the device url and DTMF tones for this meeting object must be defined.');
89
+ }
90
+
91
+ const requestParams = MembersUtil.generateSendDTMFRequestParams(options);
92
+
93
+ return this.request(requestParams);
94
+ }
95
+
96
+ /**
97
+ * @param {Object} options with format of {invitee: string, locusUrl: string}
98
+ * @returns {Promise}
99
+ * @throws {Error} if the options are not valid and complete, must have invitee with emailAddress OR email AND locusUrl
100
+ * @memberof MembersRequest
101
+ */
102
+ cancelPhoneInvite(options) {
103
+ if (!(options?.invitee?.phoneNumber || options?.locusUrl)) {
104
+ throw new ParameterError('invitee must be passed and the associated locus url for this meeting object must be defined.');
105
+ }
106
+
107
+ const requestParams = MembersUtil.generateCancelInviteRequestParams(options);
108
+
109
+ return this.request(requestParams);
110
+ }
76
111
  }
@@ -1,3 +1,5 @@
1
+ import uuid from 'uuid';
2
+
1
3
  import {
2
4
  HTTP_VERBS,
3
5
  CONTROLS,
@@ -5,7 +7,9 @@ import {
5
7
  LEAVE,
6
8
  PARTICIPANT,
7
9
  VALID_EMAIL_ADDRESS,
8
- DIALER_REGEX
10
+ DIALER_REGEX,
11
+ SEND_DTMF_ENDPOINT,
12
+ _REMOVE_
9
13
  } from '../constants';
10
14
 
11
15
  const MembersUtil = {};
@@ -156,4 +160,57 @@ MembersUtil.getTransferHostToMemberRequestParams = (options) => {
156
160
  };
157
161
  };
158
162
 
163
+ MembersUtil.genderateSendDTMFOptions = (url, tones, memberId, locusUrl) => ({
164
+ url,
165
+ tones,
166
+ memberId,
167
+ locusUrl
168
+ });
169
+
170
+ MembersUtil.generateSendDTMFRequestParams = ({
171
+ url, tones, memberId, locusUrl
172
+ }) => {
173
+ const body = {
174
+ device: {
175
+ url
176
+ },
177
+ memberId,
178
+ dtmf: {
179
+ correlationId: uuid.v4(),
180
+ tones,
181
+ direction: 'transmit'
182
+ }
183
+ };
184
+ const uri = `${locusUrl}/${PARTICIPANT}/${memberId}/${SEND_DTMF_ENDPOINT}`;
185
+
186
+ return {
187
+ method: HTTP_VERBS.POST,
188
+ uri,
189
+ body
190
+ };
191
+ };
192
+
193
+ MembersUtil.cancelPhoneInviteOptions = (invitee, locusUrl) => ({
194
+ invitee,
195
+ locusUrl
196
+ });
197
+
198
+ MembersUtil.generateCancelInviteRequestParams = (options) => {
199
+ const body = {
200
+ actionType: _REMOVE_,
201
+ invitees: [
202
+ {
203
+ address: options.invitee.phoneNumber
204
+ }
205
+ ]
206
+ };
207
+ const requestParams = {
208
+ method: HTTP_VERBS.PUT,
209
+ uri: options.locusUrl,
210
+ body
211
+ };
212
+
213
+ return requestParams;
214
+ };
215
+
159
216
  export default MembersUtil;
@@ -175,7 +175,6 @@ class Metrics {
175
175
  eventId: uuid.v4(),
176
176
  version: 1,
177
177
  origin: {
178
- buildType: 'prod',
179
178
  name: 'endpoint',
180
179
  networkType: 'unknown',
181
180
  userAgent: this.userAgentToString(),
@@ -264,7 +263,6 @@ class Metrics {
264
263
  origin: {
265
264
  audioSetupDelay,
266
265
  videoSetupDelay,
267
- buildType: 'prod',
268
266
  name: 'endpoint',
269
267
  networkType: options.networkType || UNKNOWN,
270
268
  userAgent: this.userAgentToString(),
@@ -192,8 +192,10 @@ export default class RoapHandler extends StatelessWebexPlugin {
192
192
  handleAction(session, action, meeting, correlationId) {
193
193
  let signal;
194
194
 
195
+
195
196
  switch (action.type) {
196
197
  case ROAP.RECEIVE_ROAP_MSG:
198
+ LoggerProxy.logger.log(`Roap:handler#handleAction --> RECEIVE_ROAP_MSG event captured, reciving a roap message : ${JSON.stringify(action)}`);
197
199
  if (compareWithLastRoapMessage(this.lastRoapMessage, action)) {
198
200
  LoggerProxy.logger.warn(`Roap:handler#handleAction --> duplicate roap offer from server: ${action.msg.seq}`);
199
201
  }
@@ -204,18 +206,15 @@ export default class RoapHandler extends StatelessWebexPlugin {
204
206
  }
205
207
  break;
206
208
  case ROAP.SEND_ROAP_MSG:
209
+ LoggerProxy.logger.log(`Roap:handler#handleAction --> SEND_ROAP_MSG event captured, sending roap message ${JSON.stringify(action)}`);
210
+
207
211
  action.local = true;
208
212
  this.execute(signal, session, action, meeting, ROAP.TX_);
209
213
  break;
210
214
  case ROAP.SEND_ROAP_MSG_SUCCESS:
211
- // This means we got and answer and waiting for 200 ok for /participants
212
- if (RoapCollection.getSessionSequence(correlationId, action.seq).ANSWER) {
213
- signal = ROAP.ROAP_SIGNAL.RX_ANSWER;
214
- // NOTE: When server send back an answer via mercury the
215
- // remote SDP is already saved sent and ok message is sent back
216
- // We dont have to indicate the roapHandler about the RX_ANSWER via SEND_ROAP_MSG_SUCCESS
217
- // RoapHandler.transition(signal, session, meeting);
218
- }
215
+ // NOTE: When server send back an answer via mercury the
216
+ // remote SDP is already saved sent and ok message is sent back
217
+ // We dont have to indicate the roapHandler about the RX_ANSWER via SEND_ROAP_MSG_SUCCESS
219
218
  break;
220
219
  case ROAP.RECEIVE_CALL_LEAVE:
221
220
  RoapCollection.deleteSession(correlationId);
package/src/roap/index.js CHANGED
@@ -174,6 +174,12 @@ export default class Roap extends StatelessWebexPlugin {
174
174
  seq: options.seq
175
175
  };
176
176
 
177
+ this.roapHandler.submit({
178
+ type: ROAP.SEND_ROAP_MSG,
179
+ msg: roapMessage,
180
+ correlationId: options.correlationId
181
+ });
182
+
177
183
  return this.roapRequest
178
184
  .sendRoap({
179
185
  roapMessage,
@@ -186,10 +192,11 @@ export default class Roap extends StatelessWebexPlugin {
186
192
  })
187
193
  .then(() => {
188
194
  meeting.setRoapSeq(options.seq);
195
+
189
196
  this.roapHandler.submit({
190
- type: ROAP.SEND_ROAP_MSG,
191
- msg: roapMessage,
192
- correlationId: options.correlationId
197
+ type: ROAP.SEND_ROAP_MSG_SUCCESS,
198
+ seq: roapMessage.seq,
199
+ correlationId: meeting.correlationId
193
200
  });
194
201
  });
195
202
  }
package/src/roap/state.js CHANGED
@@ -25,6 +25,8 @@ const shouldStep = (roap, meeting) => {
25
25
  };
26
26
 
27
27
  const handleTransition = (value, signal, meeting) => {
28
+ LoggerProxy.logger.log(`Roap:state#handleTransition --> current ${value} to ${signal}`);
29
+
28
30
  switch (value) {
29
31
  case ROAP.ROAP_STATE.INIT:
30
32
  if (signal === ROAP.ROAP_SIGNAL.RX_OFFER) {
@@ -186,6 +186,22 @@ describe('plugin-meetings', () => {
186
186
  assert.calledWith(meeting.members.addMember, uuid1, false);
187
187
  });
188
188
  });
189
+ describe('#cancelPhoneInvite', () => {
190
+ it('should have #invite', () => {
191
+ assert.exists(meeting.cancelPhoneInvite);
192
+ });
193
+ beforeEach(() => {
194
+ meeting.members.cancelPhoneInvite = sinon.stub().returns(Promise.resolve(test1));
195
+ });
196
+ it('should proxy members #cancelPhoneInvite and return a promise', async () => {
197
+ const cancel = meeting.cancelPhoneInvite(uuid1);
198
+
199
+ assert.exists(cancel.then);
200
+ await cancel;
201
+ assert.calledOnce(meeting.members.cancelPhoneInvite);
202
+ assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
203
+ });
204
+ });
189
205
  describe('#admit', () => {
190
206
  it('should have #admit', () => {
191
207
  assert.exists(meeting.admit);
@@ -0,0 +1,192 @@
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import sinon from 'sinon';
6
+ import uuid from 'uuid';
7
+ import chai from 'chai';
8
+ import chaiAsPromised from 'chai-as-promised';
9
+ import {Credentials} from '@webex/webex-core';
10
+ import Support from '@webex/internal-plugin-support';
11
+ import MockWebex from '@webex/test-helper-mock-webex';
12
+ import Meetings from '@webex/plugin-meetings';
13
+ import Members from '@webex/plugin-meetings/src/members';
14
+ import MembersUtil from '@webex/plugin-meetings/src/members/util';
15
+
16
+ const {assert} = chai;
17
+
18
+ chai.use(chaiAsPromised);
19
+ sinon.assert.expose(chai.assert, {prefix: ''});
20
+
21
+
22
+ describe('plugin-meetings', () => {
23
+ let webex;
24
+ let url1;
25
+ const fakeMembersCollection = {
26
+ test1: {
27
+ namespace: 'Meetings',
28
+ participant: {
29
+ state: 'JOINED',
30
+ type: 'USER',
31
+ person: {
32
+ id: '6eb08f8b-bf69-3251-a126-b161bead2d21',
33
+ phoneNumber: '+18578675309',
34
+ isExternal: true,
35
+ primaryDisplayString: '+18578675309'
36
+ },
37
+ devices: [
38
+ {
39
+ url: 'https://fakeURL.com',
40
+ deviceType: 'SIP',
41
+ state: 'JOINED',
42
+ intents: [
43
+ null
44
+ ],
45
+ correlationId: '1234',
46
+ provisionalUrl: 'dialout:///fake',
47
+ isSparkPstn: true
48
+ },
49
+ {
50
+ url: 'dialout:///fakeagain',
51
+ deviceType: 'PROVISIONAL',
52
+ state: 'JOINED',
53
+ intents: [
54
+ null
55
+ ],
56
+ correlationId: '4321',
57
+ isVideoCallback: false,
58
+ clientUrl: 'https://fakeURL',
59
+ provisionalType: 'DIAL_OUT_ONLY',
60
+ dialingStatus: 'SUCCESS'
61
+ }
62
+ ],
63
+ status: {
64
+ audioStatus: 'SENDRECV',
65
+ videoStatus: 'INACTIVE'
66
+ },
67
+ id: 'abc-123-abc-123',
68
+ guest: true,
69
+ resourceGuest: false,
70
+ moderator: false,
71
+ panelist: false,
72
+ moveToLobbyNotAllowed: true,
73
+ deviceUrl: 'https://fakeDeviceurl'
74
+ },
75
+ id: 'abc-123-abc-123',
76
+ status: 'IN_MEETING',
77
+ type: 'MEETING',
78
+ isModerator: false
79
+ }
80
+ };
81
+
82
+
83
+ describe('members', () => {
84
+ const sandbox = sinon.createSandbox();
85
+ let createMembers;
86
+
87
+ beforeEach(() => {
88
+ webex = new MockWebex({
89
+ children: {
90
+ meetings: Meetings,
91
+ credentials: Credentials,
92
+ support: Support
93
+ },
94
+ config: {
95
+ credentials: {
96
+ client_id: 'mock-client-id'
97
+ },
98
+ meetings: {
99
+ reconnection: {
100
+ enabled: false
101
+ },
102
+ mediaSettings: {},
103
+ metrics: {},
104
+ stats: {}
105
+ }
106
+ }
107
+ });
108
+
109
+ url1 = `https://example.com/${uuid.v4()}`;
110
+
111
+ createMembers = (options) => new Members({locusUrl: options.url}, {parent: webex});
112
+ });
113
+
114
+ afterEach(() => {
115
+ sandbox.restore();
116
+ });
117
+
118
+ describe('#addMembers', () => {
119
+ it('should invoke isInvalidInvitee and generateAddMemberOptions from MembersUtil when addMember is called with valid params', async () => {
120
+ sandbox.spy(MembersUtil, 'isInvalidInvitee');
121
+ sandbox.spy(MembersUtil, 'generateAddMemberOptions');
122
+
123
+ const members = createMembers({url: url1});
124
+
125
+ await members.addMember({phoneNumber: '+18578675309'});
126
+ assert.calledOnce(MembersUtil.isInvalidInvitee);
127
+ assert.calledOnce(MembersUtil.generateAddMemberOptions);
128
+ });
129
+
130
+ it('should throw a rejection if there is no locus url', async () => {
131
+ const members = createMembers({url: false});
132
+
133
+ assert.isRejected(members.addMember({email: 'test@cisco.com'}));
134
+ });
135
+ });
136
+
137
+ describe('#sendDialPadKey', () => {
138
+ it('should throw a rejection when calling sendDialPadKey with no tones', async () => {
139
+ const members = createMembers({url: url1});
140
+
141
+ assert.isRejected(members.sendDialPadKey());
142
+ });
143
+
144
+ it('should throw a rejection when calling sendDialPadKey with no member is found', async () => {
145
+ const members = createMembers({url: url1});
146
+
147
+ assert.isRejected(members.sendDialPadKey('1', '1234'));
148
+ });
149
+
150
+ it('should call genderateSendDTMFOptions with proper options on Members util if we the member is valid', async () => {
151
+ sandbox.spy(MembersUtil, 'genderateSendDTMFOptions');
152
+ const members = createMembers({url: url1});
153
+
154
+ members.membersCollection.setAll(fakeMembersCollection);
155
+ await members.sendDialPadKey('1', 'test1');
156
+ assert.calledWith(MembersUtil.genderateSendDTMFOptions, 'https://fakeURL.com', '1', 'test1', url1);
157
+ });
158
+
159
+ it('should call the sendDialPadKey method on membersRequest if the member is valid', async () => {
160
+ const members = createMembers({url: url1});
161
+
162
+ const {membersRequest} = members;
163
+
164
+ assert.exists(membersRequest);
165
+ const sendDialPadKeyspy = sandbox.spy(membersRequest, 'sendDialPadKey');
166
+
167
+ members.membersCollection.setAll(fakeMembersCollection);
168
+ await members.sendDialPadKey('1', 'test1');
169
+ assert.calledOnce(sendDialPadKeyspy);
170
+ });
171
+ });
172
+
173
+ describe('#cancelPhoneInvite', () => {
174
+ it('should invoke isInvalidInvitee and generateAddMemberOptions from MembersUtil when addMember is called with valid params', async () => {
175
+ sandbox.spy(MembersUtil, 'isInvalidInvitee');
176
+ sandbox.spy(MembersUtil, 'cancelPhoneInviteOptions');
177
+
178
+ const members = createMembers({url: url1});
179
+
180
+ await members.cancelPhoneInvite({phoneNumber: '+18578675309'});
181
+ assert.calledOnce(MembersUtil.isInvalidInvitee);
182
+ assert.calledOnce(MembersUtil.cancelPhoneInviteOptions);
183
+ });
184
+
185
+ it('should throw a rejection if there is no locus url', async () => {
186
+ const members = createMembers({url: false});
187
+
188
+ assert.isRejected(members.cancelPhoneInvite({phoneNumber: '+18578675309'}));
189
+ });
190
+ });
191
+ });
192
+ });
@@ -0,0 +1,101 @@
1
+ import sinon from 'sinon';
2
+ import chai from 'chai';
3
+ import uuid from 'uuid';
4
+ import chaiAsPromised from 'chai-as-promised';
5
+ import MockWebex from '@webex/test-helper-mock-webex';
6
+ import Meetings from '@webex/plugin-meetings';
7
+ import MembersRequest from '@webex/plugin-meetings/src/members/request';
8
+
9
+ const {assert} = chai;
10
+
11
+ chai.use(chaiAsPromised);
12
+ sinon.assert.expose(chai.assert, {prefix: ''});
13
+
14
+ describe('plugin-meetings', () => {
15
+ let membersRequest;
16
+ let url1;
17
+ let sandbox;
18
+
19
+ beforeEach(() => {
20
+ const webex = new MockWebex({
21
+ children: {
22
+ meetings: Meetings
23
+ }
24
+ });
25
+
26
+ sandbox = sinon.createSandbox();
27
+
28
+ url1 = `https://example.com/${uuid.v4()}`;
29
+
30
+ membersRequest = new MembersRequest({}, {
31
+ parent: webex
32
+ });
33
+ membersRequest.request = sinon.mock().returns(Promise.resolve({}));
34
+ });
35
+
36
+ afterEach(() => {
37
+ sandbox.restore();
38
+ });
39
+
40
+
41
+ describe('members request library', () => {
42
+ describe('#sendDialPadKey', () => {
43
+ it('sends a POST to the sendDtmf locus endpoint', async () => {
44
+ const locusUrl = url1;
45
+ const url = 'https://fakedeviceurl.com';
46
+ const tones = '1';
47
+ const memberId = 'test1';
48
+
49
+ await membersRequest.sendDialPadKey({
50
+ url,
51
+ tones,
52
+ memberId,
53
+ locusUrl
54
+ });
55
+ const requestParams = membersRequest.request.getCall(0).args[0];
56
+
57
+ assert.equal(requestParams.method, 'POST');
58
+ assert.equal(requestParams.uri, `${locusUrl}/participant/${memberId}/sendDtmf`);
59
+ assert.equal(requestParams.body.dtmf.tones, tones);
60
+ assert.equal(requestParams.body.device.url, url);
61
+ });
62
+ });
63
+
64
+ describe('#addMembers', () => {
65
+ it('sends a PUT to the locus endpoint', async () => {
66
+ const options = {
67
+ invitee: {
68
+ phoneNumber: '+18578675309'
69
+ },
70
+ locusUrl: url1
71
+ };
72
+
73
+ await membersRequest.addMembers(options);
74
+ const requestParams = membersRequest.request.getCall(0).args[0];
75
+
76
+ assert.equal(requestParams.method, 'PUT');
77
+ assert.equal(requestParams.uri, url1);
78
+ assert.equal(requestParams.body.invitees[0].address, '+18578675309');
79
+ });
80
+ });
81
+
82
+ describe('#cancelPhoneInvite', () => {
83
+ it('sends a PUT to the locus endpoint', async () => {
84
+ const options = {
85
+ invitee: {
86
+ phoneNumber: '+18578675309'
87
+ },
88
+ locusUrl: url1
89
+ };
90
+
91
+ await membersRequest.cancelPhoneInvite(options);
92
+ const requestParams = membersRequest.request.getCall(0).args[0];
93
+
94
+ assert.equal(requestParams.method, 'PUT');
95
+ assert.equal(requestParams.uri, url1);
96
+ assert.equal(requestParams.body.invitees[0].address, '+18578675309');
97
+ assert.equal(requestParams.body.actionType, 'REMOVE');
98
+ });
99
+ });
100
+ });
101
+ });