Skip to content

Commit

Permalink
feat: support slack block kits and view events
Browse files Browse the repository at this point in the history
  • Loading branch information
darkbtf committed Dec 18, 2019
1 parent 06e350a commit 4802c59
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 2 deletions.
136 changes: 136 additions & 0 deletions packages/bottender/src/context/SlackContext.ts
@@ -0,0 +1,136 @@
import sleep from 'delay';
import warning from 'warning';
import { SlackOAuthClient, SlackTypes } from 'messaging-api-slack';

import Context from './Context';
import SlackEvent from './SlackEvent';
import { PlatformContext } from './PlatformContext';

export default class SlackContext extends Context<SlackOAuthClient, SlackEvent>
implements PlatformContext {
/**
* The name of the platform.
*
*/
get platform(): string {
return 'slack';
}

/**
* Delay and show indicators for milliseconds.
*
*/
async typing(milliseconds: number): Promise<void> {
if (milliseconds > 0) {
await sleep(milliseconds);
}
}

/**
* Sends a message to the channel of the session.
*
* https://api.slack.com/methods/chat.postMessage
*/
postMessage(
message:
| {
text?: string;
attachments?: SlackTypes.Attachment[] | string;
blocks?: SlackTypes.Block[] | string;
}
| string,
options?: {}
): Promise<any> {
const channelId = this._getChannelIdFromSession();

if (!channelId) {
warning(
false,
'postMessage: should not be called in context without session'
);
return Promise.resolve();
}

this._isHandled = true;

return this._client.postMessage(channelId, message, {
threadTs: this._event.rawEvent.thread_ts,
...options,
});
}

/**
* Sends an ephemeral message to the session owner.
*
* https://api.slack.com/methods/chat.postMessage
*/
postEphemeral(
message:
| {
text?: string;
attachments?: SlackTypes.Attachment[] | string;
blocks?: SlackTypes.Block[] | string;
}
| string,
options?: {}
): Promise<any> {
const channelId = this._getChannelIdFromSession();

if (!channelId) {
warning(
false,
'postMessage: should not be called in context without session'
);
return Promise.resolve();
}

this._isHandled = true;

return this._client.postEphemeral(
channelId,
(this._session as any).user.id,
message,
options
);
}

/**
* Open a view (modal) and use private_metadata to keep channelId for session key
*
* https://api.slack.com/methods/view.open
*/
openView(view: {}, triggerId: string): Promise<any> {
return this._client.callMethod('views.open', {
view: JSON.stringify({
...view,
private_metadata: JSON.stringify({
...(view.private_metadata || {}),
channelId: this.event.rawEvent.channel.id,
}),
}),
trigger_id: triggerId,
});
}

/**
* Send text to the owner of the session.
*
*/
sendText(text: string, options?: {}): Promise<any> {
return this.postMessage(text, options);
}

// FIXME: this is to fix type checking
_getChannelIdFromSession(): string | null {
if (!this._session) return null;
if (
typeof this._session.channel === 'object' &&
this._session.channel &&
this._session.channel.id &&
typeof this._session.channel.id === 'string'
) {
return this._session.channel.id;
}
return null;
}
}
9 changes: 8 additions & 1 deletion packages/bottender/src/slack/SlackConnector.ts
Expand Up @@ -138,6 +138,11 @@ export default class SlackConnector
return rawEvent.channelId;
}

// For slack modal
if (rawEvent.view && rawEvent.view.private_metadata) {
return JSON.parse(rawEvent.view.private_metadata).channelId;
}

// For reaction_added format
if (
rawEvent.item &&
Expand All @@ -159,7 +164,9 @@ export default class SlackConnector
let userFromBody;
if (
rawEvent.type === 'interactive_message' ||
rawEvent.type === 'block_actions'
rawEvent.type === 'block_actions' ||
rawEvent.type === 'view_submission' ||
rawEvent.type === 'view_closed')
) {
userFromBody = rawEvent.user.id;
} else {
Expand Down
23 changes: 22 additions & 1 deletion packages/bottender/src/slack/SlackEvent.ts
Expand Up @@ -102,10 +102,15 @@ export type BlockActionEvent = UIEvent & {
type: 'block_actions';
};

export type ViewEvent = UIEvent & {
type: 'view_submission' | 'view_closed';
};

export type SlackRawEvent =
| Message
| InteractiveMessageEvent
| BlockActionEvent;
| BlockActionEvent
| ViewEvent;

export default class SlackEvent implements Event<SlackRawEvent> {
_rawEvent: SlackRawEvent;
Expand Down Expand Up @@ -225,6 +230,22 @@ export default class SlackEvent implements Event<SlackRawEvent> {
return this._rawEvent.type === 'block_actions';
}

/**
* Determine if the event is a view submission event.
*
*/
get isViewSubmission(): boolean {
return this._rawEvent.type === 'view_submission';
}

/**
* Determine if the event is a view closed event.
*
*/
get isViewClosed(): boolean {
return this._rawEvent.type === 'view_closed';
}

/**
* Determine if the event is an UI Event (block actions or interactive message)
*
Expand Down

0 comments on commit 4802c59

Please sign in to comment.