Skip to content

Commit e81864a

Browse files
feat: surface the setStatus argument to listeners if required event details are available (#2843)
1 parent 175e0b1 commit e81864a

11 files changed

Lines changed: 251 additions & 27 deletions

File tree

src/App.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import {
1212
type SlackCustomFunctionMiddlewareArgs,
1313
} from './CustomFunction';
1414
import type { WorkflowStep } from './WorkflowStep';
15-
import { createFunctionComplete, createFunctionFail, createRespond, createSay, createSayStream } from './context';
16-
import type { SayStreamFn } from './context';
15+
import {
16+
createFunctionComplete,
17+
createFunctionFail,
18+
createRespond,
19+
createSay,
20+
createSayStream,
21+
createSetStatus,
22+
} from './context';
23+
import type { SayStreamFn, SetStatusFn } from './context';
1724
import { type ConversationStore, MemoryStore, conversationContext } from './conversation-store';
1825
import {
1926
AppInitializationError,
@@ -1054,6 +1061,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
10541061
say?: SayFn;
10551062
/** SayStream function might be set below */
10561063
sayStream?: SayStreamFn;
1064+
/** SetStatus function might be set below */
1065+
setStatus?: SetStatusFn;
10571066
/** Respond function might be set below */
10581067
respond?: RespondFn;
10591068
/** Ack function might be set below */
@@ -1122,6 +1131,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
11221131
const resolvedThreadTs = threadTs ?? eventTs;
11231132
if (resolvedThreadTs !== undefined) {
11241133
listenerArgs.sayStream = createSayStream(client, context, eventChannelId, resolvedThreadTs);
1134+
listenerArgs.setStatus = createSetStatus(client, eventChannelId, resolvedThreadTs);
11251135
}
11261136
}
11271137
} else if (type === IncomingEventType.Action) {

src/Assistant.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import type {
2-
AssistantThreadsSetStatusArguments,
3-
AssistantThreadsSetStatusResponse,
42
AssistantThreadsSetSuggestedPromptsResponse,
53
AssistantThreadsSetTitleResponse,
64
ChatPostMessageArguments,
@@ -11,8 +9,9 @@ import {
119
type AssistantThreadContextStore,
1210
DefaultThreadContextStore,
1311
} from './AssistantThreadContextStore';
14-
import { createSayStream } from './context';
12+
import { createSayStream, createSetStatus } from './context';
1513
import type { SayStreamFn } from './context';
14+
import type { SetStatusFn } from './context';
1615
import { AssistantInitializationError, AssistantMissingPropertyError } from './errors';
1716
import { extractEventChannelId, extractEventThreadTs, isRecord } from './helpers';
1817
import processMiddleware from './middleware/process';
@@ -43,9 +42,6 @@ interface AssistantUtilityArgs {
4342

4443
type GetThreadContextUtilFn = () => Promise<AssistantThreadContext>;
4544
type SaveThreadContextUtilFn = () => Promise<void>;
46-
type SetStatusFn = (
47-
status: string | Omit<AssistantThreadsSetStatusArguments, 'channel_id' | 'thread_ts'>,
48-
) => Promise<AssistantThreadsSetStatusResponse>;
4945

5046
type SetSuggestedPromptsFn = (
5147
params: SetSuggestedPromptsArguments,
@@ -186,7 +182,7 @@ export function enrichAssistantArgs(
186182

187183
preparedArgs.say = createSay(preparedArgs);
188184
preparedArgs.sayStream = createAssistantSayStream(preparedArgs);
189-
preparedArgs.setStatus = createSetStatus(preparedArgs);
185+
preparedArgs.setStatus = createAssistantSetStatus(preparedArgs);
190186
preparedArgs.setSuggestedPrompts = createSetSuggestedPrompts(preparedArgs);
191187
preparedArgs.setTitle = createSetTitle(preparedArgs);
192188
return preparedArgs;
@@ -350,24 +346,10 @@ function createAssistantSayStream(args: AllAssistantMiddlewareArgs): SayStreamFn
350346
* Creates utility `setStatus()` to set the status and indicate active processing.
351347
* https://api.slack.com/methods/assistant.threads.setStatus
352348
*/
353-
function createSetStatus(args: AllAssistantMiddlewareArgs): SetStatusFn {
349+
function createAssistantSetStatus(args: AllAssistantMiddlewareArgs): SetStatusFn {
354350
const { client, payload } = args;
355-
const { channelId: channel_id, threadTs: thread_ts } = extractThreadInfo(payload);
356-
357-
return (status: Parameters<SetStatusFn>[0]): Promise<AssistantThreadsSetStatusResponse> => {
358-
if (typeof status === 'string') {
359-
return client.assistant.threads.setStatus({
360-
channel_id,
361-
thread_ts,
362-
status,
363-
});
364-
}
365-
return client.assistant.threads.setStatus({
366-
channel_id,
367-
thread_ts,
368-
...status,
369-
});
370-
};
351+
const { channelId, threadTs } = extractThreadInfo(payload);
352+
return createSetStatus(client, channelId, threadTs);
371353
}
372354

373355
/**

src/context/create-set-status.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { AssistantThreadsSetStatusArguments, AssistantThreadsSetStatusResponse, WebClient } from '@slack/web-api';
2+
3+
export type SetStatusArguments = Omit<AssistantThreadsSetStatusArguments, 'channel_id' | 'thread_ts'>;
4+
5+
export type SetStatusFn = (status: string | SetStatusArguments) => Promise<AssistantThreadsSetStatusResponse>;
6+
7+
export function createSetStatus(client: WebClient, channelId: string, threadTs: string): SetStatusFn {
8+
return (status: string | SetStatusArguments) => {
9+
if (typeof status === 'string') {
10+
return client.assistant.threads.setStatus({
11+
channel_id: channelId,
12+
thread_ts: threadTs,
13+
status,
14+
});
15+
}
16+
return client.assistant.threads.setStatus({
17+
channel_id: channelId,
18+
thread_ts: threadTs,
19+
...status,
20+
});
21+
};
22+
}

src/context/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { createSay } from './create-say';
22
export { createSayStream } from './create-say-stream';
33
export type { SayStreamFn, SayStreamArguments } from './create-say-stream';
4+
export { createSetStatus } from './create-set-status';
5+
export type { SetStatusFn, SetStatusArguments } from './create-set-status';
46
export { createRespond } from './create-respond';
57
export { createFunctionComplete } from './create-function-complete';
68
export { createFunctionFail } from './create-function-fail';

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export * from './errors';
5353
export * from './middleware/builtin';
5454
export * from './types';
5555
export type { SayStreamFn, SayStreamArguments } from './context/create-say-stream';
56+
export type { SetStatusFn, SetStatusArguments } from './context/create-set-status';
5657

5758
export { ConversationStore, MemoryStore } from './conversation-store';
5859

src/types/events/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { FunctionExecutedEvent, SlackEvent } from '@slack/types';
22
import type { FunctionCompleteFn, FunctionFailFn } from '../../CustomFunction';
33
import type { SayStreamFn } from '../../context/create-say-stream';
4+
import type { SetStatusFn } from '../../context/create-set-status';
45
import type { AckFn, SayFn, StringIndexed } from '../utilities';
56

67
export type SlackEventMiddlewareArgsOptions = { autoAcknowledge: boolean };
@@ -49,7 +50,7 @@ export type SlackEventMiddlewareArgs<EventType extends string = string> = {
4950
: unknown) &
5051
(EventFromType<EventType> extends EventWithChannelContext
5152
? EventFromType<EventType> extends EventWithThreadTsContext | EventWithTsContext
52-
? { sayStream: SayStreamFn }
53+
? { sayStream: SayStreamFn; setStatus: SetStatusFn }
5354
: unknown
5455
: unknown) &
5556
(EventType extends 'function_executed'

test/types/set-status.test-d.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { expectType } from 'tsd';
2+
import type { SetStatusFn } from '../../';
3+
import App from '../../src/App';
4+
5+
const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' });
6+
7+
app.event('message', async ({ setStatus }) => {
8+
expectType<SetStatusFn>(setStatus);
9+
});
10+
11+
app.event('app_mention', async ({ setStatus }) => {
12+
expectType<SetStatusFn>(setStatus);
13+
});
14+
15+
app.event('assistant_thread_started', async ({ setStatus }) => {
16+
expectType<SetStatusFn>(setStatus);
17+
});
18+
19+
app.message('*', async ({ setStatus }) => {
20+
expectType<SetStatusFn>(setStatus);
21+
});
22+
23+
app.event('reaction_added', async (args) => {
24+
// @ts-expect-error - setStatus not available without ts context
25+
const _status: SetStatusFn = args.setStatus;
26+
});
27+
28+
app.event('app_home_opened', async (args) => {
29+
// @ts-expect-error - setStatus not available without ts context
30+
const _status: SetStatusFn = args.setStatus;
31+
});
32+
33+
app.event('message_metadata_posted', async (args) => {
34+
// @ts-expect-error - setStatus not available without ts context
35+
const _status: SetStatusFn = args.setStatus;
36+
});
37+
38+
app.event('channel_created', async (args) => {
39+
// @ts-expect-error - setStatus not available without ts context
40+
const _status: SetStatusFn = args.setStatus;
41+
});
42+
43+
app.event('user_huddle_changed', async (args) => {
44+
// @ts-expect-error - setStatus should not exist on events without channel context
45+
const _status: SetStatusFn = args.setStatus;
46+
});
47+
48+
app.action('button_click', async (args) => {
49+
// @ts-expect-error - setStatus should not exist on action listeners
50+
const _status: SetStatusFn = args.setStatus;
51+
});
52+
53+
app.command('/slash-command', async (args) => {
54+
// @ts-expect-error - setStatus should not exist on command listeners
55+
const _status: SetStatusFn = args.setStatus;
56+
});
57+
58+
app.function('sample-func', async (args) => {
59+
// @ts-expect-error - setStatus should not exist on function listeners
60+
const _status: SetStatusFn = args.setStatus;
61+
});
62+
63+
app.view('my-view', async (args) => {
64+
// @ts-expect-error - setStatus should not exist on view listeners
65+
const _status: SetStatusFn = args.setStatus;
66+
});
67+
68+
app.options('my-options', async (args) => {
69+
// @ts-expect-error - setStatus should not exist on option listeners
70+
const _status: SetStatusFn = args.setStatus;
71+
});

test/unit/App/middlewares/arguments.spec.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { assert } from 'chai';
33
import sinon, { type SinonSpy } from 'sinon';
44
import { LogLevel } from '../../../../src/App';
55
import type { SayStreamFn } from '../../../../src/context/create-say-stream';
6+
import type { SetStatusFn } from '../../../../src/context/create-set-status';
67
import type { ReceiverEvent, SayFn } from '../../../../src/types';
78
import {
89
FakeReceiver,
@@ -25,6 +26,7 @@ import {
2526
withNoopAppMetadata,
2627
withNoopWebClient,
2728
withPostMessage,
29+
withSetStatus,
2830
withSuccessfulBotUserFetchingWebClient,
2931
} from '../../helpers';
3032

@@ -759,6 +761,65 @@ describe('App middleware and listener arguments', () => {
759761
});
760762
});
761763

764+
describe('setStatus()', () => {
765+
it('should be available for events with channel and ts context', async () => {
766+
const fakeSetStatus = sinon.fake.resolves({ ok: true });
767+
overrides = buildOverrides([withSetStatus(fakeSetStatus)]);
768+
const MockApp = importApp(overrides);
769+
770+
const assertionAggregator = sinon.fake();
771+
const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
772+
app.use(async (args) => {
773+
// biome-ignore lint/suspicious/noExplicitAny: test utility
774+
const setStatus = (args as any).setStatus as SetStatusFn;
775+
assert.isFunction(setStatus);
776+
assertionAggregator();
777+
});
778+
app.error(fakeErrorHandler);
779+
780+
// Event with channel and ts (message event)
781+
await fakeReceiver.sendEvent({
782+
...baseEvent,
783+
body: {
784+
event: {
785+
type: 'message',
786+
channel: dummyChannelId,
787+
ts: '1234.5678',
788+
},
789+
team_id: 'TEAM_ID',
790+
},
791+
});
792+
793+
sinon.assert.calledOnce(assertionAggregator);
794+
sinon.assert.notCalled(fakeErrorHandler);
795+
});
796+
797+
it('should not be available for events without channel context', async () => {
798+
overrides = buildOverrides([withNoopWebClient()]);
799+
const MockApp = importApp(overrides);
800+
801+
const assertionAggregator = sinon.fake();
802+
const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) });
803+
app.use(async (args) => {
804+
assert.notProperty(args, 'setStatus');
805+
assertionAggregator();
806+
});
807+
808+
// Event without channel context
809+
await fakeReceiver.sendEvent({
810+
...baseEvent,
811+
body: {
812+
event: {
813+
type: 'tokens_revoked',
814+
},
815+
team_id: 'TEAM_ID',
816+
},
817+
});
818+
819+
sinon.assert.calledOnce(assertionAggregator);
820+
});
821+
});
822+
762823
describe('ack()', () => {
763824
it('should be available in middleware/listener args', async () => {
764825
const MockApp = importApp(overrides);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { WebClient } from '@slack/web-api';
2+
import { assert } from 'chai';
3+
import sinon from 'sinon';
4+
import { createSetStatus } from '../../../src/context';
5+
6+
describe('createSetStatus', () => {
7+
const sandbox = sinon.createSandbox();
8+
const client = new WebClient('token');
9+
let setStatusStub: sinon.SinonStub;
10+
11+
beforeEach(() => {
12+
setStatusStub = sandbox.stub(client.assistant.threads, 'setStatus').resolves({
13+
ok: true,
14+
});
15+
});
16+
17+
afterEach(() => {
18+
sandbox.restore();
19+
});
20+
21+
it('should call client.assistant.threads.setStatus with string status', () => {
22+
const setStatus = createSetStatus(client, 'C1234', '1234.5678');
23+
setStatus('is thinking...');
24+
25+
assert(setStatusStub.calledOnce);
26+
const args = setStatusStub.firstCall.args[0];
27+
28+
assert.equal(args.channel_id, 'C1234');
29+
assert.equal(args.thread_ts, '1234.5678');
30+
assert.equal(args.status, 'is thinking...');
31+
});
32+
33+
it('should call client.assistant.threads.setStatus with object status', () => {
34+
const setStatus = createSetStatus(client, 'C1234', '1234.5678');
35+
setStatus({ status: 'is thinking...', loading_messages: ['Loading...', 'Still working...'] });
36+
37+
assert(setStatusStub.calledOnce);
38+
const args = setStatusStub.firstCall.args[0];
39+
40+
assert.equal(args.channel_id, 'C1234');
41+
assert.equal(args.thread_ts, '1234.5678');
42+
assert.equal(args.status, 'is thinking...');
43+
assert.deepEqual(args.loading_messages, ['Loading...', 'Still working...']);
44+
});
45+
46+
it('should use the channel_id and thread_ts from factory creation', () => {
47+
const setStatus = createSetStatus(client, 'C9999', '9999.0000');
48+
setStatus('processing');
49+
50+
assert(setStatusStub.calledOnce);
51+
const args = setStatusStub.firstCall.args[0];
52+
53+
assert.equal(args.channel_id, 'C9999');
54+
assert.equal(args.thread_ts, '9999.0000');
55+
});
56+
});

test/unit/helpers/app.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ export function withChatStream(spy: SinonSpy): Override {
117117
};
118118
}
119119

120+
export function withSetStatus(spy: SinonSpy): Override {
121+
return {
122+
'@slack/web-api': {
123+
WebClient: class {
124+
public assistant = {
125+
threads: {
126+
setStatus: spy,
127+
},
128+
};
129+
},
130+
},
131+
};
132+
}
133+
120134
export function withAxiosPost(spy: SinonSpy): Override {
121135
return {
122136
axios: {

0 commit comments

Comments
 (0)