diff --git a/.changeset/friendly-sockets-listen.md b/.changeset/friendly-sockets-listen.md new file mode 100644 index 000000000..8ac8cd14f --- /dev/null +++ b/.changeset/friendly-sockets-listen.md @@ -0,0 +1,5 @@ +--- +'@slack/socket-mode': patch +--- + +Fix Events API dispatch for payloads without an inner event, including app_rate_limited. Keep malformed payloads available to generic slack_event listeners for acknowledgement. diff --git a/packages/socket-mode/src/SocketModeClient.test.ts b/packages/socket-mode/src/SocketModeClient.test.ts index e68ff270f..eed6574b0 100644 --- a/packages/socket-mode/src/SocketModeClient.test.ts +++ b/packages/socket-mode/src/SocketModeClient.test.ts @@ -152,6 +152,104 @@ describe('SocketModeClient', () => { }); describe('events_api messages', () => { + class TestSocketModeClient extends SocketModeClient { + async receive(payload: unknown): Promise { + await this.onWebSocketMessage(JSON.stringify(payload), false); + } + } + + it('should dispatch the inner event of an event_callback', async () => { + const client = new TestSocketModeClient({ appToken: 'xapp-' }); + const event = { type: 'app_mention', text: '<@U111>' }; + const body = { type: 'event_callback', event }; + const specific = sandbox.spy(); + const generic = sandbox.spy(); + const callback = sandbox.spy(); + client.on('app_mention', specific); + client.on('event_callback', callback); + client.on('slack_event', generic); + + await client.receive({ type: 'events_api', payload: body, envelope_id: 'inner-event' }); + + sinon.assert.calledOnce(specific); + sinon.assert.notCalled(callback); + assert.deepEqual(specific.firstCall.args[0].body, body); + assert.deepEqual(specific.firstCall.args[0].event, event); + sinon.assert.calledOnce(generic); + assert.deepEqual(generic.firstCall.args[0].body, body); + }); + + it('should dispatch app_rate_limited without an inner event', async () => { + const client = new TestSocketModeClient({ appToken: 'xapp-' }); + const body = { type: 'app_rate_limited', team_id: 'T111', minute_rate_limited: 1610241741 }; + const specific = sandbox.spy(); + const generic = sandbox.spy(); + client.on('app_rate_limited', specific); + client.on('slack_event', generic); + + await client.receive({ + type: 'events_api', + payload: body, + envelope_id: 'rate-limited', + retry_attempt: 2, + retry_reason: 'timeout', + accepts_response_payload: false, + }); + + sinon.assert.calledOnce(specific); + const args = specific.firstCall.args[0]; + assert.deepEqual(args.body, body); + assert.strictEqual(args.event, args.body); + assert.strictEqual(args.envelope_id, 'rate-limited'); + assert.strictEqual(args.retry_num, 2); + assert.strictEqual(args.retry_reason, 'timeout'); + assert.strictEqual(args.accepts_response_payload, false); + sinon.assert.calledOnce(generic); + assert.deepEqual(generic.firstCall.args[0].body, body); + assert.strictEqual(generic.firstCall.args[0].ack, args.ack); + }); + + it('should allow generic listeners to ACK malformed payloads without a valid event type', async () => { + for (const body of [ + {}, + { event: null }, + { event: { type: 123 }, type: 456 }, + { event: { type: '' }, type: '' }, + null, + undefined, + ]) { + const client = new TestSocketModeClient({ appToken: 'xapp-' }); + const send = sandbox + .stub(client as unknown as { send: (id: string, response: unknown) => Promise }, 'send') + .resolves(); + const generic = sandbox.spy(); + const emit = sandbox.spy(client, 'emit'); + client.on('slack_event', generic); + + await client.receive({ type: 'events_api', payload: body, envelope_id: 'malformed' }); + + sinon.assert.calledOnce(emit); + sinon.assert.calledOnce(generic); + const args = generic.firstCall.args[0]; + assert.strictEqual(args.type, 'events_api'); + assert.deepEqual(args.body, body); + await args.ack({}); + sinon.assert.calledOnceWithExactly(send, 'malformed', {}); + } + }); + + it('should use the payload type when the inner event type is invalid', async () => { + const client = new TestSocketModeClient({ appToken: 'xapp-' }); + const body = { type: 'app_rate_limited', event: { type: 123 } }; + const specific = sandbox.spy(); + client.on('app_rate_limited', specific); + + await client.receive({ type: 'events_api', payload: body, envelope_id: 'invalid-inner-event' }); + + sinon.assert.calledOnce(specific); + assert.deepEqual(specific.firstCall.args[0].event, body); + }); + const envelopeId = 'cda4159a-72a5-4744-aba3-4d66eb52682b'; const appMention = JSON.stringify({ envelope_id: envelopeId, diff --git a/packages/socket-mode/src/SocketModeClient.ts b/packages/socket-mode/src/SocketModeClient.ts index 93bf8c7c0..5d69a67c1 100644 --- a/packages/socket-mode/src/SocketModeClient.ts +++ b/packages/socket-mode/src/SocketModeClient.ts @@ -363,15 +363,22 @@ export class SocketModeClient extends EventEmitter { // For events_api messages, expose the type of the event if (event.type === 'events_api') { - this.emit(event.payload.event.type, { - ack, - envelope_id: event.envelope_id, - body: event.payload, - event: event.payload.event, - retry_num: event.retry_attempt, - retry_reason: event.retry_reason, - accepts_response_payload: event.accepts_response_payload, - }); + const innerEvent = event.payload?.event; + const dispatchedEvent = + typeof innerEvent?.type === 'string' && innerEvent.type.length > 0 ? innerEvent : event.payload; + if (typeof dispatchedEvent?.type === 'string' && dispatchedEvent.type.length > 0) { + this.emit(dispatchedEvent.type, { + ack, + envelope_id: event.envelope_id, + body: event.payload, + event: dispatchedEvent, + retry_num: event.retry_attempt, + retry_reason: event.retry_reason, + accepts_response_payload: event.accepts_response_payload, + }); + } else { + this.logger.debug(`Received an events_api message without an event type (envelope_id: ${event.envelope_id})`); + } } else { // Emit just ack and body for all other types of messages this.emit(event.type, {