From f79bc633c032f2fe40846316b737beb64d48c71d Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 30 Apr 2021 19:44:12 -0500 Subject: [PATCH] :sparkles: Extend Twist Node (#1721) * Add get/getAll:messageConversation to Twist node * Add delete:messageConversation to Twist node * Add update:messageConversation to Twist node * Add archive/unarchive/delete:channel to Twist node * Add add/update/get/getAll/remove:Thread to Twist node * Add add/update/get/getAll/remove:Comment to Twist node * Lint fixes * Fix operations's descriptions * Enhance Twist node code * Reorder attributes alphabetically * Fix typos * Fix the ouput of get:Comment operation * Fix getAll:Comment & getAll:Thread operations outputs * :bug: Add missing scopes and remove not needed parameters Co-authored-by: dali --- .../credentials/TwistOAuth2Api.credentials.ts | 2 + .../nodes/Twist/ChannelDescription.ts | 44 +- .../nodes/Twist/CommentDescription.ts | 561 +++++++++++++++++ .../Twist/MessageConversationDescription.ts | 315 +++++++++- .../nodes/Twist/ThreadDescription.ts | 567 ++++++++++++++++++ packages/nodes-base/nodes/Twist/Twist.node.ts | 482 ++++++++++++++- 6 files changed, 1946 insertions(+), 25 deletions(-) create mode 100644 packages/nodes-base/nodes/Twist/CommentDescription.ts create mode 100644 packages/nodes-base/nodes/Twist/ThreadDescription.ts diff --git a/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts index 850a53dabc..3c6b820791 100644 --- a/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts @@ -6,7 +6,9 @@ import { const scopes = [ 'attachments:write', 'channels:remove', + 'comments:remove', 'messages:remove', + 'threads:remove', 'workspaces:read', ]; diff --git a/packages/nodes-base/nodes/Twist/ChannelDescription.ts b/packages/nodes-base/nodes/Twist/ChannelDescription.ts index 6412125330..fd0e33a3eb 100644 --- a/packages/nodes-base/nodes/Twist/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Twist/ChannelDescription.ts @@ -15,11 +15,21 @@ export const channelOperations = [ }, }, options: [ + { + name: 'Archive', + value: 'archive', + description: 'Archive a channel', + }, { name: 'Create', value: 'create', description: 'Initiates a public or private channel-based conversation', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a channel', + }, { name: 'Get', value: 'get', @@ -30,6 +40,11 @@ export const channelOperations = [ value: 'getAll', description: 'Get all channels', }, + { + name: 'Unarchive', + value: 'unarchive', + description: 'Unarchive a channel', + }, { name: 'Update', value: 'update', @@ -64,7 +79,7 @@ export const channelFields = [ }, }, required: true, - description: 'The id of the workspace.', + description: 'The ID of the workspace.', }, { displayName: 'Name', @@ -156,28 +171,28 @@ export const channelFields = [ }, ], default: 0, - description: 'The color of the channel', + description: 'The color of the channel.', }, { displayName: 'Description', name: 'description', type: 'string', default: '', - description: 'The description of the channel', + description: 'The description of the channel.', }, { displayName: 'Public', name: 'public', type: 'boolean', default: false, - description: 'If enabled, the channel will be marked as public', + description: 'If enabled, the channel will be marked as public.', }, { displayName: 'Temp ID', name: 'temp_id', type: 'number', default: -1, - description: 'The temporary id of the channel. It needs to be a negative number.', + description: 'The temporary ID of the channel. It needs to be a negative number.', }, { displayName: 'User IDs', @@ -194,8 +209,9 @@ export const channelFields = [ }, ], }, + /* -------------------------------------------------------------------------- */ - /* channel:get */ + /* channel:get/archive/unarchive/delete */ /* -------------------------------------------------------------------------- */ { displayName: 'Channel ID', @@ -205,7 +221,10 @@ export const channelFields = [ displayOptions: { show: { operation: [ + 'archive', + 'delete', 'get', + 'unarchive', ], resource: [ 'channel', @@ -213,8 +232,9 @@ export const channelFields = [ }, }, required: true, - description: 'The ID of the channel', + description: 'The ID of the channel.', }, + /* -------------------------------------------------------------------------- */ /* channel:getAll */ /* -------------------------------------------------------------------------- */ @@ -302,7 +322,7 @@ export const channelFields = [ name: 'archived', type: 'boolean', default: false, - description: 'If enabled, only archived conversations are returned', + description: 'If enabled, only archived conversations are returned.', }, ], }, @@ -400,28 +420,28 @@ export const channelFields = [ }, ], default: 0, - description: 'The color of the channel', + description: 'The color of the channel.', }, { displayName: 'Description', name: 'description', type: 'string', default: '', - description: 'The description of the channel', + description: 'The description of the channel.', }, { displayName: 'Name', name: 'name', type: 'string', default: '', - description: 'The name of the channel', + description: 'The name of the channel.', }, { displayName: 'Public', name: 'public', type: 'boolean', default: false, - description: 'If enabled, the channel will be marked as public', + description: 'If enabled, the channel will be marked as public.', }, ], }, diff --git a/packages/nodes-base/nodes/Twist/CommentDescription.ts b/packages/nodes-base/nodes/Twist/CommentDescription.ts new file mode 100644 index 0000000000..0aa0500d1a --- /dev/null +++ b/packages/nodes-base/nodes/Twist/CommentDescription.ts @@ -0,0 +1,561 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const commentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'comment', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new comment to a thread', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a comment', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a comment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all comments', + }, + { + name: 'Update', + value: 'update', + description: 'Update a comment', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const commentFields = [ + /*-------------------------------------------------------------------------- */ + /* comment:create */ + /* ------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The content of the comment.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Mark thread position', + name: 'mark_thread_position', + type: 'boolean', + default: true, + description: 'By default, the position of the thread is marked.', + }, + { + displayName: 'Recipients', + name: 'recipients', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that will attached to the comment.', + }, + { + displayName: 'Temporary ID', + name: 'temp_id', + type: 'number', + default: 0, + description: 'The temporary ID of the comment.', + }, + { + displayName: 'Send as integration', + name: 'send_as_integration', + type: 'boolean', + default: false, + description: 'Displays the integration as the comment creator.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* comment:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get', + 'delete', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the comment.', + }, + + /* -------------------------------------------------------------------------- */ + /* comment:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'As IDs', + name: 'as_ids', + type: 'boolean', + default: false, + description: 'If enabled, only the ids of the comments are returned.', + }, + { + displayName: 'Ending Object Index', + name: 'to_obj_index', + type: 'number', + default: 50, + description: 'Limit comments ending at the specified object index.', + }, + { + displayName: 'Newer Than', + name: 'newer_than_ts', + type: 'dateTime', + default: '', + description: 'Limits comments to those newer when the specified Unix time.', + }, + { + displayName: 'Older Than', + name: 'older_than_ts', + type: 'dateTime', + default: '', + description: 'Limits comments to those older than the specified Unix time.', + }, + { + displayName: 'Order By', + name: 'order_by', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: 'ASC', + description: 'The order of the comments returned - one of DESC or ASC.', + }, + { + displayName: 'Starting Object Index', + name: 'from_obj_index', + type: 'number', + default: 0, + description: 'Limit comments starting at the specified object index.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* comment:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Comment ID', + name: 'commentId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'comment', + ], + }, + }, + required: true, + description: 'The ID of the comment.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'comment', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the comment.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts index d81f1254dc..c08201339e 100644 --- a/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts +++ b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts @@ -20,6 +20,26 @@ export const messageConversationOperations = [ value: 'create', description: 'Create a message in a conversation', }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a message in a conversation', + }, + { + name: 'Get', + value: 'get', + description: 'Get a message in a conversation', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all messages in a conversation', + }, + { + name: 'Update', + value: 'update', + description: 'Update a message in a conversation', + }, ], default: 'create', description: 'The operation to perform.', @@ -91,7 +111,7 @@ export const messageConversationFields = [ ], }, }, - description: `The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.`, + description: 'The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.', }, { displayName: 'Additional Fields', @@ -108,7 +128,7 @@ export const messageConversationFields = [ }, }, default: {}, - description: 'Other options to set', + description: 'Other options to set.', placeholder: 'Add options', options: [ { @@ -128,7 +148,7 @@ export const messageConversationFields = [ displayName: 'Action', name: 'action', type: 'options', - description: 'The action of the button', + description: 'The action of the button.', options: [ { name: 'Open URL', @@ -171,7 +191,7 @@ export const messageConversationFields = [ displayName: 'Type', name: 'type', type: 'options', - description: 'The type of the button, for now just action is available.', + description: 'The type of the button. (Currently only action is available).', options: [ { name: 'Action', @@ -191,7 +211,7 @@ export const messageConversationFields = [ ], }, }, - description: 'URL to redirect', + description: 'URL to redirect.', default: '', }, ], @@ -213,7 +233,7 @@ export const messageConversationFields = [ loadOptionsMethod: 'getUsers', }, default: [], - description: `The users that are directly mentioned`, + description: 'The users that are directly mentioned.', }, // { // displayName: 'Direct Group Mentions ', @@ -223,8 +243,289 @@ export const messageConversationFields = [ // loadOptionsMethod: 'getGroups', // }, // default: [], - // description: `The groups that are directly mentioned`, + // description: 'The groups that are directly mentioned.', // }, ], }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the workspace.', + }, + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getConversations', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'messageConversation', + ], + }, + }, + default: {}, + description: 'Other options to set.', + options: [ + { + displayName: 'Ending Object Index', + name: 'to_obj_index', + type: 'number', + default: 50, + description: 'Limit messages ending at the specified object index.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Limits the number of messages returned.', + }, + { + displayName: 'Order By', + name: 'order_by', + type: 'options', + default: 'ASC', + description: 'The order of the conversations returned - one of DESC or ASC.', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + }, + { + displayName: 'Starting Object Index', + name: 'from_obj_index', + type: 'number', + default: 0, + description: 'Limit messages starting at the specified object index.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:get/delete/update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'delete', + 'get', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation message.', + }, + + /* -------------------------------------------------------------------------- */ + /* messageConversation:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Conversation Message ID', + name: 'id', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation message.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'messageConversation', + ], + }, + }, + default: {}, + description: 'Other options to set.', + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: 'The users that are directly mentioned.', + }, + ], + }, ] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/ThreadDescription.ts b/packages/nodes-base/nodes/Twist/ThreadDescription.ts new file mode 100644 index 0000000000..4ce8f48a47 --- /dev/null +++ b/packages/nodes-base/nodes/Twist/ThreadDescription.ts @@ -0,0 +1,567 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const threadOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'thread', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new thread in a channel', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a thread', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a thread', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all threads', + }, + { + name: 'Update', + value: 'update', + description: 'Update a thread', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const threadFields = [ + /*-------------------------------------------------------------------------- */ + /* thread:create */ + /* ------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The title of the new thread (1 < length < 300).', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The content of the thread.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Recipients', + name: 'recipients', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that will attached to the thread.', + }, + { + displayName: 'Send as integration', + name: 'send_as_integration', + type: 'boolean', + default: false, + description: 'Displays the integration as the thread creator.', + }, + { + displayName: 'Temporary ID', + name: 'temp_id', + type: 'number', + default: 0, + description: 'The temporary ID of the thread.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* thread:get/delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get', + 'delete', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + /* -------------------------------------------------------------------------- */ + /* thread:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'As IDs', + name: 'as_ids', + type: 'boolean', + default: false, + description: 'If enabled, only the IDs of the threads are returned.', + }, + { + displayName: 'Filter By', + name: 'filter_by', + type: 'options', + options: [ + { + name: 'Attached to me', + value: 'attached_to_me', + }, + { + name: 'Everyone', + value: 'everyone', + }, + { + name: 'Starred', + value: 'is_starred', + }, + ], + default: '', + description: 'A filter can be one of attached_to_me, everyone and is_starred.', + }, + { + displayName: 'Newer Than', + name: 'newer_than_ts', + type: 'dateTime', + default: '', + description: 'Limits threads to those newer when the specified Unix time.', + }, + { + displayName: 'Older Than', + name: 'older_than_ts', + type: 'dateTime', + default: '', + description: 'Limits threads to those older than the specified Unix time.', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* thread:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Thread ID', + name: 'threadId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'thread', + ], + }, + }, + required: true, + description: 'The ID of the thread.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'thread', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button.', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button. (Currently only action is available).', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + description: 'The content of the thread.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that are directly mentioned.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'The title of the thread (1 < length < 300).', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/Twist.node.ts b/packages/nodes-base/nodes/Twist/Twist.node.ts index d2fe9f18dd..3d9c70092c 100644 --- a/packages/nodes-base/nodes/Twist/Twist.node.ts +++ b/packages/nodes-base/nodes/Twist/Twist.node.ts @@ -29,7 +29,16 @@ import { messageConversationOperations, } from './MessageConversationDescription'; +import { + threadFields, + threadOperations +} from './ThreadDescription'; +import { + commentFields, + commentOperations +} from './CommentDescription'; import uuid = require('uuid'); +import * as moment from 'moment'; export class Twist implements INodeType { description: INodeTypeDescription = { @@ -62,18 +71,30 @@ export class Twist implements INodeType { name: 'Channel', value: 'channel', }, + { + name: 'Comment', + value: 'comment', + }, { name: 'Message Conversation', value: 'messageConversation', }, + { + name: 'Thread', + value: 'thread', + }, ], default: 'messageConversation', description: 'The resource to operate on.', }, ...channelOperations, ...channelFields, + ...commentOperations, + ...commentFields, ...messageConversationOperations, ...messageConversationFields, + ...threadOperations, + ...threadFields, ], }; @@ -169,10 +190,15 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/channels/add', body); } + //https://developer.twist.com/v3/#remove-channel + if (operation === 'delete') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/remove', {}, qs); + } //https://developer.twist.com/v3/#get-channel if (operation === 'get') { - const channelId = this.getNodeParameter('channelId', i) as string; - qs.id = channelId; + qs.id = this.getNodeParameter('channelId', i) as string; responseData = await twistApiRequest.call(this, 'GET', '/channels/getone', {}, qs); } @@ -202,6 +228,190 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/channels/update', body); } + //https://developer.twist.com/v3/#archive-channel + if (operation === 'archive') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/archive', {}, qs); + } + //https://developer.twist.com/v3/#unarchive-channel + if (operation === 'unarchive') { + qs.id = this.getNodeParameter('channelId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/channels/unarchive', {}, qs); + } + } + if (resource === 'comment') { + //https://developer.twist.com/v3/#add-comment + if (operation === 'create') { + const threadId = this.getNodeParameter('threadId', i) as string; + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + thread_id: threadId, + content, + }; + Object.assign(body, additionalFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/comments/add', body); + } + //https://developer.twist.com/v3/#remove-comment + if (operation === 'delete') { + qs.id = this.getNodeParameter('commentId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/comments/remove', {}, qs); + } + //https://developer.twist.com/v3/#get-comment + if (operation === 'get') { + qs.id = this.getNodeParameter('commentId', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/comments/getone', {}, qs); + responseData = responseData?.comment; + } + //https://developer.twist.com/v3/#get-all-comments + if (operation === 'getAll') { + const threadId = this.getNodeParameter('threadId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.thread_id = threadId; + + Object.assign(qs, filters); + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + if (qs.older_than_ts) { + qs.older_than_ts = moment(qs.older_than_ts as string).unix(); + } + if (qs.newer_than_ts) { + qs.newer_than_ts = moment(qs.newer_than_ts as string).unix(); + } + + responseData = await twistApiRequest.call(this, 'GET', '/comments/get', {}, qs); + if (qs.as_ids) { + responseData = (responseData as Array).map(id => ({ ID: id })); + } + } + //https://developer.twist.com/v3/#update-comment + if (operation === 'update') { + const commentId = this.getNodeParameter('commentId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id: commentId, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/comments/update', body); + } } if (resource === 'messageConversation') { //https://developer.twist.com/v3/#add-message-to-conversation @@ -244,7 +454,7 @@ export class Twist implements INodeType { attachments.push(await twistApiRequest.call( this, 'POST', - `/attachments/upload`, + '/attachments/upload', {}, {}, { @@ -265,11 +475,11 @@ export class Twist implements INodeType { } if (body.direct_mentions) { - const direcMentions: string[] = []; + const directMentions: string[] = []; for (const directMention of body.direct_mentions as number[]) { - direcMentions.push(`[name](twist-mention://${directMention})`); + directMentions.push(`[name](twist-mention://${directMention})`); } - body.content = `${direcMentions.join(' ')} ${body.content}`; + body.content = `${directMentions.join(' ')} ${body.content}`; } // if (body.direct_group_mentions) { @@ -282,6 +492,266 @@ export class Twist implements INodeType { responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/add', body); } + //https://developer.twist.com/v3/#get-message + if (operation === 'get') { + qs.id = this.getNodeParameter('id', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/conversation_messages/getone', {}, qs); + } + //https://developer.twist.com/v3/#get-all-messages + if (operation === 'getAll') { + const conversationId = this.getNodeParameter('conversationId', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + qs.conversation_id = conversationId; + Object.assign(qs, additionalFields); + + responseData = await twistApiRequest.call(this, 'GET', '/conversation_messages/get', {}, qs); + } + //https://developer.twist.com/v3/#remove-message-from-conversation + if (operation === 'delete') { + qs.id = this.getNodeParameter('id', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/remove', {}, qs); + } + //https://developer.twist.com/v3/#update-message-in-conversation + if (operation === 'update') { + const id = this.getNodeParameter('id', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/update', body); + } + } + if (resource === 'thread') { + //https://developer.twist.com/v3/#add-thread + if (operation === 'create') { + const channelId = this.getNodeParameter('channelId', i) as string; + const title = this.getNodeParameter('title', i) as string; + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + channel_id: channelId, + content, + title, + }; + Object.assign(body, additionalFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/threads/add', body); + } + //https://developer.twist.com/v3/#remove-thread + if (operation === 'delete') { + qs.id = this.getNodeParameter('threadId', i) as string; + + responseData = await twistApiRequest.call(this, 'POST', '/threads/remove', {}, qs); + } + //https://developer.twist.com/v3/#get-thread + if (operation === 'get') { + qs.id = this.getNodeParameter('threadId', i) as string; + + responseData = await twistApiRequest.call(this, 'GET', '/threads/getone', {}, qs); + } + //https://developer.twist.com/v3/#get-all-threads + if (operation === 'getAll') { + const channelId = this.getNodeParameter('channelId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.channel_id = channelId; + + Object.assign(qs, filters); + if (!returnAll) { + qs.limit = this.getNodeParameter('limit', i) as number; + } + if (qs.older_than_ts) { + qs.older_than_ts = moment(qs.older_than_ts as string).unix(); + } + if (qs.newer_than_ts) { + qs.newer_than_ts = moment(qs.newer_than_ts as string).unix(); + } + + responseData = await twistApiRequest.call(this, 'GET', '/threads/get', {}, qs); + if (qs.as_ids) { + responseData = (responseData as Array).map(id => ({ ID: id })); + } + } + //https://developer.twist.com/v3/#update-thread + if (operation === 'update') { + const threadId = this.getNodeParameter('threadId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id: threadId, + }; + Object.assign(body, updateFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + '/attachments/upload', + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const directMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + directMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${directMentions.join(' ')} ${body.content}`; + } + + responseData = await twistApiRequest.call(this, 'POST', '/threads/update', body); + } } if (Array.isArray(responseData)) { returnData.push.apply(returnData, responseData as IDataObject[]);