@mbakgun/n8n-nodes-slack-socket-mode
Version:
Slack Socket Mode Node for n8n that allows you to use +100 Slack events in your n8n instance with proxy mode
1,011 lines (1,010 loc) • 52.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SlackSocketTrigger = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const bolt_1 = require("@slack/bolt");
const https_proxy_agent_1 = require("https-proxy-agent");
class SlackSocketTrigger {
constructor() {
this.description = {
displayName: 'Slack Socket Mode Trigger',
name: 'slackSocketTrigger',
group: ['trigger'],
version: 2,
description: 'Triggers workflow when a Slack message matches a regex pattern via Socket Mode',
defaults: {
name: 'Slack Socket Mode Trigger',
},
icon: 'file:./assets/slack-socket-mode.svg',
inputs: [],
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
credentials: [
{
name: 'slackSocketCredentialsApi',
required: true,
},
],
properties: [
{
displayName: 'Trigger On',
name: 'trigger',
type: 'multiOptions',
options: [
{
name: 'App Deleted',
value: 'app_deleted',
description: 'When a user has deleted an app',
},
{
name: 'App Home Opened',
value: 'app_home_opened',
description: 'When a user clicks into your App Home',
},
{
name: 'App Installed',
value: 'app_installed',
description: 'When a user has installed an app',
},
{
name: 'App Mention',
value: 'app_mention',
description: 'When your bot or app is mentioned in a channel',
},
{
name: 'App Rate Limited',
value: 'app_rate_limited',
description: 'When your app\'s event subscriptions are being rate limited',
},
{
name: 'App Requested',
value: 'app_requested',
description: 'When a user requested an app',
},
{
name: 'App Uninstalled',
value: 'app_uninstalled',
description: 'When your Slack app was uninstalled',
},
{
name: 'App Uninstalled Team',
value: 'app_uninstalled_team',
description: 'When a user has uninstalled an app',
},
{
name: 'Assistant Thread Context Changed',
value: 'assistant_thread_context_changed',
description: 'When the context changed while an AI assistant thread was visible',
},
{
name: 'Assistant Thread Started',
value: 'assistant_thread_started',
description: 'When an AI assistant thread was started',
},
{
name: 'Bot Added',
value: 'bot_added',
description: 'When a bot user was added',
},
{
name: 'Bot Changed',
value: 'bot_changed',
description: 'When a bot user was changed',
},
{
name: 'Button Interaction',
value: 'block_actions',
description: 'When a user interacts with buttons, including NPS-style ratings',
},
{
name: 'Call Rejected',
value: 'call_rejected',
description: 'When a call was rejected',
},
{
name: 'Channel Archive',
value: 'channel_archive',
description: 'When a channel was archived',
},
{
name: 'Channel Created',
value: 'channel_created',
description: 'When a new channel was created',
},
{
name: 'Channel Deleted',
value: 'channel_deleted',
description: 'When a channel was deleted',
},
{
name: 'Channel History Changed',
value: 'channel_history_changed',
description: 'When bulk updates were made to a channel\'s history',
},
{
name: 'Channel ID Changed',
value: 'channel_id_changed',
description: 'When a channel ID changed',
},
{
name: 'Channel Joined',
value: 'channel_joined',
description: 'When you joined a channel',
},
{
name: 'Channel Left',
value: 'channel_left',
description: 'When you left a channel',
},
{
name: 'Channel Marked',
value: 'channel_marked',
description: 'When your channel read marker was updated',
},
{
name: 'Channel Rename',
value: 'channel_rename',
description: 'When a channel was renamed',
},
{
name: 'Channel Shared',
value: 'channel_shared',
description: 'When a channel has been shared with an external workspace',
},
{
name: 'Channel Unarchive',
value: 'channel_unarchive',
description: 'When a channel was unarchived',
},
{
name: 'Channel Unshared',
value: 'channel_unshared',
description: 'When a channel has been unshared with an external workspace',
},
{
name: 'Commands Changed',
value: 'commands_changed',
description: 'When a slash command has been added or changed',
},
{
name: 'DND Updated',
value: 'dnd_updated',
description: 'When Do not Disturb settings changed for the current user',
},
{
name: 'DND Updated User',
value: 'dnd_updated_user',
description: 'When Do not Disturb settings changed for a member',
},
{
name: 'Email Domain Changed',
value: 'email_domain_changed',
description: 'When the workspace email domain has changed',
},
{
name: 'Emoji Changed',
value: 'emoji_changed',
description: 'When a custom emoji has been added or changed',
},
{
name: 'External Org Migration Finished',
value: 'external_org_migration_finished',
description: 'When an enterprise grid migration has finished on an external workspace',
},
{
name: 'External Org Migration Started',
value: 'external_org_migration_started',
description: 'When an enterprise grid migration has started on an external workspace',
},
{
name: 'File Change',
value: 'file_change',
description: 'When a file was changed',
},
{
name: 'File Comment Added',
value: 'file_comment_added',
description: 'When a file comment was added',
},
{
name: 'File Comment Deleted',
value: 'file_comment_deleted',
description: 'When a file comment was deleted',
},
{
name: 'File Comment Edited',
value: 'file_comment_edited',
description: 'When a file comment was edited',
},
{
name: 'File Created',
value: 'file_created',
description: 'When a file was created',
},
{
name: 'File Deleted',
value: 'file_deleted',
description: 'When a file was deleted',
},
{
name: 'File Public',
value: 'file_public',
description: 'When a file was made public',
},
{
name: 'File Shared',
value: 'file_shared',
description: 'When a file was shared',
},
{
name: 'File Unshared',
value: 'file_unshared',
description: 'When a file was unshared',
},
{
name: 'Function Executed',
value: 'function_executed',
description: 'When your app function is executed as a step in a workflow',
},
{
name: 'Grid Migration Finished',
value: 'grid_migration_finished',
description: 'When an enterprise grid migration has finished on this workspace',
},
{
name: 'Grid Migration Started',
value: 'grid_migration_started',
description: 'When an enterprise grid migration has started on this workspace',
},
{
name: 'Group Archive',
value: 'group_archive',
description: 'When a private channel was archived',
},
{
name: 'Group Close',
value: 'group_close',
description: 'When you closed a private channel',
},
{
name: 'Group Deleted',
value: 'group_deleted',
description: 'When a private channel was deleted',
},
{
name: 'Group History Changed',
value: 'group_history_changed',
description: 'When bulk updates were made to a private channel\'s history',
},
{
name: 'Group Joined',
value: 'group_joined',
description: 'When you joined a private channel',
},
{
name: 'Group Left',
value: 'group_left',
description: 'When you left a private channel',
},
{
name: 'Group Marked',
value: 'group_marked',
description: 'When a private channel read marker was updated',
},
{
name: 'Group Open',
value: 'group_open',
description: 'When you created a group DM',
},
{
name: 'Group Rename',
value: 'group_rename',
description: 'When a private channel was renamed',
},
{
name: 'Group Unarchive',
value: 'group_unarchive',
description: 'When a private channel was unarchived',
},
{
name: 'Hello',
value: 'hello',
description: 'When the client has successfully connected to the server',
},
{
name: 'IM Close',
value: 'im_close',
description: 'When you closed a DM',
},
{
name: 'IM Created',
value: 'im_created',
description: 'When a DM was created',
},
{
name: 'IM History Changed',
value: 'im_history_changed',
description: 'When bulk updates were made to a DM\'s history',
},
{
name: 'IM Marked',
value: 'im_marked',
description: 'When a direct message read marker was updated',
},
{
name: 'IM Open',
value: 'im_open',
description: 'When you opened a DM',
},
{
name: 'Invite Requested',
value: 'invite_requested',
description: 'When a user requested an invite',
},
{
name: 'Link Shared',
value: 'link_shared',
description: 'When a message was posted containing links relevant to your application',
},
{
name: 'Manual Presence Change',
value: 'manual_presence_change',
description: 'When you manually updated your presence',
},
{
name: 'Member Joined Channel',
value: 'member_joined_channel',
description: 'When a user joined a public channel, private channel or MPDM',
},
{
name: 'Member Left Channel',
value: 'member_left_channel',
description: 'When a user left a public or private channel',
},
{
name: 'Message',
value: 'message',
description: 'When a message was sent to a channel',
},
{
name: 'Message App Home',
value: 'message.app_home',
description: 'When a user sent a message to your Slack app',
},
{
name: 'Message Channels',
value: 'message.channels',
description: 'When a message was posted to a channel',
},
{
name: 'Message Groups',
value: 'message.groups',
description: 'When a message was posted to a private channel',
},
{
name: 'Message IM',
value: 'message.im',
description: 'When a message was posted in a direct message channel',
},
{
name: 'Message Metadata Deleted',
value: 'message_metadata_deleted',
description: 'When message metadata was deleted',
},
{
name: 'Message Metadata Posted',
value: 'message_metadata_posted',
description: 'When message metadata was posted',
},
{
name: 'Message Metadata Updated',
value: 'message_metadata_updated',
description: 'When message metadata was updated',
},
{
name: 'Message MPIM',
value: 'message.mpim',
description: 'When a message was posted in a multiparty direct message channel',
},
{
name: 'Pin Added',
value: 'pin_added',
description: 'When a pin was added to a channel',
},
{
name: 'Pin Removed',
value: 'pin_removed',
description: 'When a pin was removed from a channel',
},
{
name: 'Pref Change',
value: 'pref_change',
description: 'When you have updated your preferences',
},
{
name: 'Presence Change',
value: 'presence_change',
description: 'When a member\'s presence changed',
},
{
name: 'Reaction Added',
value: 'reaction_added',
description: 'When a member has added an emoji reaction to an item',
},
{
name: 'Reaction Removed',
value: 'reaction_removed',
description: 'When a member removed an emoji reaction',
},
{
name: 'Resources Added',
value: 'resources_added',
description: 'When access to a set of resources was granted for your app',
},
{
name: 'Resources Removed',
value: 'resources_removed',
description: 'When access to a set of resources was removed for your app',
},
{
name: 'Scope Denied',
value: 'scope_denied',
description: 'When OAuth scopes were denied to your app',
},
{
name: 'Scope Granted',
value: 'scope_granted',
description: 'When OAuth scopes were granted to your app',
},
{
name: 'Shared Channel Invite Accepted',
value: 'shared_channel_invite_accepted',
description: 'When a shared channel invite was accepted',
},
{
name: 'Shared Channel Invite Approved',
value: 'shared_channel_invite_approved',
description: 'When a shared channel invite was approved',
},
{
name: 'Shared Channel Invite Declined',
value: 'shared_channel_invite_declined',
description: 'When a shared channel invite was declined',
},
{
name: 'Shared Channel Invite Received',
value: 'shared_channel_invite_received',
description: 'When a shared channel invite was sent to a Slack user',
},
{
name: 'Shared Channel Invite Requested',
value: 'shared_channel_invite_requested',
description: 'When a shared channel invite was requested',
},
{
name: 'Slash Command',
value: 'slash_command',
description: 'When a user invokes a slash command (e.g., /mycommand)',
},
{
name: 'Star Added',
value: 'star_added',
description: 'When a member has saved an item for later or starred an item',
},
{
name: 'Star Removed',
value: 'star_removed',
description: 'When a member has removed an item saved for later or starred an item',
},
{
name: 'Subteam Created',
value: 'subteam_created',
description: 'When a User Group has been added to the workspace',
},
{
name: 'Subteam Members Changed',
value: 'subteam_members_changed',
description: 'When the membership of an existing User Group has changed',
},
{
name: 'Subteam Self Added',
value: 'subteam_self_added',
description: 'When you have been added to a User Group',
},
{
name: 'Subteam Self Removed',
value: 'subteam_self_removed',
description: 'When you have been removed from a User Group',
},
{
name: 'Subteam Updated',
value: 'subteam_updated',
description: 'When an existing User Group has been updated or its members changed',
},
{
name: 'Team Access Granted',
value: 'team_access_granted',
description: 'When access to a set of teams was granted to your org app',
},
{
name: 'Team Access Revoked',
value: 'team_access_revoked',
description: 'When access to a set of teams was revoked from your org app',
},
{
name: 'Team Domain Change',
value: 'team_domain_change',
description: 'When the workspace domain has changed',
},
{
name: 'Team Join',
value: 'team_join',
description: 'When a new member has joined',
},
{
name: 'Team Plan Change',
value: 'team_plan_change',
description: 'When the account billing plan has changed',
},
{
name: 'Team Pref Change',
value: 'team_pref_change',
description: 'When a preference has been updated',
},
{
name: 'Team Profile Change',
value: 'team_profile_change',
description: 'When the workspace profile fields have been updated',
},
{
name: 'Team Profile Delete',
value: 'team_profile_delete',
description: 'When the workspace profile fields have been deleted',
},
{
name: 'Team Profile Reorder',
value: 'team_profile_reorder',
description: 'When the workspace profile fields have been reordered',
},
{
name: 'Team Rename',
value: 'team_rename',
description: 'When the workspace name has changed',
},
{
name: 'Tokens Revoked',
value: 'tokens_revoked',
description: 'When API tokens for your app were revoked',
},
{
name: 'URL Verification',
value: 'url_verification',
description: 'When verifying ownership of an Events API Request URL',
},
{
name: 'User Change',
value: 'user_change',
description: 'When a member\'s data has changed',
},
{
name: 'User Resource Denied',
value: 'user_resource_denied',
description: 'When user resource was denied to your app',
},
{
name: 'User Resource Granted',
value: 'user_resource_granted',
description: 'When user resource was granted to your app',
},
{
name: 'User Resource Removed',
value: 'user_resource_removed',
description: 'When user resource was removed from your app',
},
{
name: 'User Typing',
value: 'user_typing',
description: 'When a channel member is typing a message',
},
{
name: 'View Closed',
value: 'view_closed',
description: 'When a modal view is closed (contains view details and private_metadata)',
},
{
name: 'View Submitted',
value: 'view_submission',
description: 'When a modal view is submitted (contains submitted values)',
},
{
name: 'Workflow Deleted',
value: 'workflow_deleted',
description: 'When a workflow that contains a step supported by your app was deleted',
},
{
name: 'Workflow Published',
value: 'workflow_published',
description: 'When a workflow that contains a step supported by your app was published',
},
{
name: 'Workflow Step Deleted',
value: 'workflow_step_deleted',
description: 'When a workflow step supported by your app was removed from a workflow',
},
{
name: 'Workflow Step Execute',
value: 'workflow_step_execute',
description: 'When a workflow step supported by your app should execute',
},
{
name: 'Workflow Unpublished',
value: 'workflow_unpublished',
description: 'When a workflow that contains a step supported by your app was unpublished',
},
],
default: [],
},
{
displayName: 'Regex Pattern',
name: 'regexPattern',
type: 'string',
default: '',
placeholder: 'regex pattern',
description: 'Regular expression to match against incoming Slack messages. Capture groups can be used to extract data.',
},
{
displayName: 'Regex Flags',
name: 'regexFlags',
type: 'string',
default: 'g',
placeholder: 'gmi',
description: 'Flags for the regular expression (e.g., g for global, i for case-insensitive)',
},
{
displayName: 'Slash Commands',
name: 'slashCommand',
type: 'string',
default: '',
placeholder: 'deploy, status, help',
description: 'Comma-separated list of slash commands to listen for (e.g., deploy, status, help). Leave empty to listen for all slash commands. Do not include the leading slash.',
displayOptions: {
show: {
trigger: ['slash_command'],
},
},
},
{
displayName: 'Channels to Watch',
name: 'channelsToWatch',
type: 'fixedCollection',
default: {},
placeholder: 'Add Channel',
description: 'Select channels to filter events. If specified, only events from these channels will trigger the workflow. To enter IDs manually, add each channel separately (e.g., C1234567890, G1234567890).',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Channel',
},
options: [
{
name: 'channelValues',
displayName: 'Channel',
values: [
{
displayName: 'Channel',
name: 'channel',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel',
description: 'Channel to watch for events',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a channel',
typeOptions: {
searchListMethod: 'channelSearch',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'C1234567890',
validation: [
{
type: 'regex',
properties: {
regex: '^[C|G|D][A-Z0-9]{8,}$',
errorMessage: 'Not a valid Slack channel ID',
},
},
],
},
],
},
],
},
],
},
{
displayName: 'Legacy Channel to Watch',
name: 'channelToWatch',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
placeholder: 'Select a channel',
description: 'Legacy single-channel selector retained for workflows created before version 1.4.0',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a channel',
typeOptions: {
searchListMethod: 'channelSearch',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
type: 'string',
placeholder: 'C1234567890',
validation: [
{
type: 'regex',
properties: {
regex: '^[C|G|D][A-Z0-9]{8,}$',
errorMessage: 'Not a valid Slack channel ID',
},
},
],
},
],
},
],
};
this.methods = {
listSearch: {
async channelSearch() {
var _a;
const credentials = await this.getCredentials('slackSocketCredentialsApi');
if (!credentials.botToken) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Bot token is required to load channels');
}
const app = new bolt_1.App({
token: credentials.botToken,
signingSecret: credentials.signingSecret,
appToken: credentials.appToken,
});
try {
const result = await app.client.conversations.list({
types: 'public_channel,private_channel',
exclude_archived: true,
});
const results = [];
if (result.channels) {
for (const channel of result.channels) {
if (channel.name && channel.id) {
results.push({
name: `#${channel.name}`,
value: channel.id,
description: ((_a = channel.purpose) === null || _a === void 0 ? void 0 : _a.value) || '',
});
}
}
}
return { results };
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to load channels: ${error}`);
}
},
},
};
}
async trigger() {
var _a;
const filters = this.getNodeParameter('trigger', []);
const pattern = this.getNodeParameter('regexPattern');
const flags = this.getNodeParameter('regexFlags');
const slashCommand = this.getNodeParameter('slashCommand', '');
const channelsToWatch = this.getNodeParameter('channelsToWatch', {});
const legacyChannelToWatch = this.getNodeParameter('channelToWatch', null);
const channelIds = [];
if ((channelsToWatch === null || channelsToWatch === void 0 ? void 0 : channelsToWatch.channelValues) && Array.isArray(channelsToWatch.channelValues)) {
for (const item of channelsToWatch.channelValues) {
if (((_a = item === null || item === void 0 ? void 0 : item.channel) === null || _a === void 0 ? void 0 : _a.value) && typeof item.channel.value === 'string') {
channelIds.push(item.channel.value);
}
}
}
if ((legacyChannelToWatch === null || legacyChannelToWatch === void 0 ? void 0 : legacyChannelToWatch.value) && typeof legacyChannelToWatch.value === 'string' && legacyChannelToWatch.value.length > 0) {
channelIds.push(legacyChannelToWatch.value);
}
const uniqueChannelIds = Array.from(new Set(channelIds));
const regExp = pattern.length > 0 ? new RegExp(pattern, flags) : undefined;
let credentials;
try {
credentials = await this.getCredentials('slackSocketCredentialsApi');
}
catch (error) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Failed to get Slack Socket credentials: ' + error);
}
if (!credentials.botToken || !credentials.appToken || !credentials.signingSecret) {
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Missing required Slack Socket credentials');
}
const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
const agent = proxyUrl ? new https_proxy_agent_1.HttpsProxyAgent(proxyUrl) : undefined;
const socketModeReceiver = new bolt_1.SocketModeReceiver({
appToken: credentials.appToken,
installerOptions: {
clientOptions: { agent },
},
});
socketModeReceiver.client.clientPingTimeoutMS = 20000;
socketModeReceiver.client.serverPingTimeoutMS = 60000;
const app = new bolt_1.App({
token: credentials.botToken,
signingSecret: credentials.signingSecret,
receiver: socketModeReceiver,
});
const getEventChannelId = (event) => {
var _a, _b, _c;
if (!event)
return null;
if (event.channel) {
return event.channel;
}
if ((_a = event.item) === null || _a === void 0 ? void 0 : _a.channel) {
return event.item.channel;
}
if ((_b = event.item) === null || _b === void 0 ? void 0 : _b.channel_id) {
return event.item.channel_id;
}
if (((_c = event.file) === null || _c === void 0 ? void 0 : _c.channels) && Array.isArray(event.file.channels) && event.file.channels.length > 0) {
return event.file.channels[0];
}
return null;
};
let isStopped = false;
const socketProcess = async (root) => {
if (isStopped) {
return;
}
try {
const event = root === null || root === void 0 ? void 0 : root.event;
if (uniqueChannelIds.length > 0 && event) {
const eventChannelId = getEventChannelId(event);
if (eventChannelId && !uniqueChannelIds.includes(eventChannelId)) {
return;
}
}
const sanitized = {};
for (const key of Object.keys(root)) {
const val = root[key];
if (typeof val !== 'function') {
sanitized[key] = val;
}
}
this.emit([this.helpers.returnJsonArray(sanitized)]);
}
catch (error) {
this.logger.error('Error processing Slack Socket event: ' + error);
}
};
const setupEventListeners = () => {
filters.forEach((filter) => {
try {
if (filter === 'message') {
const handleMessageEvent = async (args) => {
const event = args === null || args === void 0 ? void 0 : args.event;
if (!event) {
return;
}
if (regExp) {
const jsonString = JSON.stringify(event);
regExp.lastIndex = 0;
if (!jsonString || !regExp.test(jsonString)) {
return;
}
}
await socketProcess(args);
};
app.event('message', handleMessageEvent);
}
else if (filter === 'block_actions') {
app.action(/.*/, async (args) => {
const { ack } = args;
await ack();
await socketProcess(args);
});
;
}
else if (filter === 'view_submission') {
app.view({ type: 'view_submission' }, async (args) => {
try {
this.logger.info('view_submission received');
if (typeof args.ack === 'function') {
await args.ack();
}
}
catch (err) {
this.logger.error('Error acknowledging view_submission: ' + err);
}
await socketProcess(args);
});
}
else if (filter === 'view_closed') {
app.view({ type: 'view_closed' }, async (args) => {
try {
this.logger.info('view_closed received');
if (typeof args.ack === 'function') {
await args.ack();
}
}
catch (err) {
this.logger.error('view_closed ack error (safe to ignore): ' + err);
}
await socketProcess(args);
});
}
else if (filter === 'slash_command') {
const commandHandler = async (args) => {
const { ack, command } = args;
try {
await ack();
}
catch (err) {
this.logger.error('Error acknowledging slash command: ' + err);
}
if (regExp && command) {
const commandText = command.text || '';
regExp.lastIndex = 0;
if (!regExp.test(commandText)) {
return;
}
}
await socketProcess(args);
};
const commands = slashCommand
.split(',')
.map((cmd) => cmd.trim())
.filter((cmd) => cmd.length > 0)
.map((cmd) => cmd.startsWith('/') ? cmd.slice(1) : cmd);
if (commands.length > 0) {
for (const cmd of commands) {
app.command(`/${cmd}`, commandHandler);
this.logger.info(`Listening for slash command: /${cmd}`);
}
}
else {
app.command(/.*/, commandHandler);
this.logger.info('Listening for all slash commands');
}
}
else if (filter.startsWith('message.')) {
const channelType = filter.replace('message.', '');
const channelTypeMap = {
'channels': ['channel'],
'groups': ['group'],
'im': ['im'],
'mpim': ['mpim'],
'app_home': ['app_home', 'im'],
};
const actualChannelTypes = channelTypeMap[channelType] || [channelType];
const handleMessageSubtypeEvent = async (args) => {
const event = args === null || args === void 0 ? void 0 : args.event;
if (!event || !actualChannelTypes.includes(event.channel_type)) {
return;
}
if (regExp) {
const jsonString = JSON.stringify(event);
regExp.lastIndex = 0;
if (!jsonString || !regExp.test(jsonString)) {
return;
}
}
await socketProcess(args);
};
app.event('message', handleMessageSubtypeEvent);