Add Microsoft Teams Node (#931)

*  Microsoft Team node

*  Small fix

*  Small improvements

*  Small improvements

*  Small fix

*  Small improvements
This commit is contained in:
Ricardo Espinoza 2020-09-13 05:08:43 -04:00 committed by GitHub
parent 542e772e0c
commit 38ddcbe703
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 924 additions and 2 deletions

View file

@ -11,17 +11,19 @@ export class MicrosoftOAuth2Api implements ICredentialType {
displayName = 'Microsoft OAuth2 API';
documentationUrl = 'microsoft';
properties = [
//info about the tenantID
//https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'string' as NodePropertyTypes,
default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/authorize',
default: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'string' as NodePropertyTypes,
default: 'https://login.microsoftonline.com/{yourtenantid}/oauth2/v2.0/token',
default: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
},
{
displayName: 'Auth URI Query Parameters',

View file

@ -0,0 +1,21 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MicrosoftTeamsOAuth2Api implements ICredentialType {
name = 'microsoftTeamsOAuth2Api';
extends = [
'microsoftOAuth2Api',
];
displayName = 'Microsoft OAuth2 API';
properties = [
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'openid offline_access User.ReadWrite.All Group.ReadWrite.All',
},
];
}

View file

@ -0,0 +1,381 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const channelOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'channel',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a channel',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a channel',
},
{
name: 'Get',
value: 'get',
description: 'Get a channel',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all channels',
},
{
name: 'Update',
value: 'update',
description: 'Update a channel',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const channelFields = [
/* -------------------------------------------------------------------------- */
/* channel:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Name',
name: 'name',
required: true,
type: 'string',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Channel name as it will appear to the user in Microsoft Teams.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channel',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: `channel's description`,
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Private',
value: 'private',
},
{
name: 'Standard',
value: 'standard',
},
],
default: 'standard',
description: 'The type of the channel',
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
loadOptionsDependsOn: [
'teamId',
],
},
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'channel',
],
},
},
default: '',
description: 'channel ID',
},
/* -------------------------------------------------------------------------- */
/* channel:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
loadOptionsDependsOn: [
'teamId',
],
},
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'channel',
],
},
},
default: '',
description: 'channel ID',
},
/* -------------------------------------------------------------------------- */
/* channel:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channel',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channel',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* channel:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
loadOptionsDependsOn: [
'teamId',
],
},
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'channel',
],
},
},
default: '',
description: 'Channel ID',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'channel',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Channel name as it will appear to the user in Microsoft Teams.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: `channel's description`,
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,220 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const channelMessageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'channelMessage',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a message',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all messages',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const channelMessageFields = [
/* -------------------------------------------------------------------------- */
/* channelMessage:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
loadOptionsDependsOn: [
'teamId',
],
},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'Channel ID',
},
{
displayName: 'Message Type',
name: 'messageType',
required: true,
type: 'options',
options: [
{
name: 'Text',
value: 'text',
},
{
name: 'HTML',
value: 'html',
},
],
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'The type of the content',
},
{
displayName: 'Message',
name: 'message',
required: true,
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'The content of the item.',
},
/* -------------------------------------------------------------------------- */
/* channelMessage:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Team ID',
name: 'teamId',
required: true,
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTeams',
},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'Team ID',
},
{
displayName: 'Channel ID',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
loadOptionsDependsOn: [
'teamId',
],
},
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channelMessage',
],
},
},
default: '',
description: 'Channel ID',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channelMessage',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'channelMessage',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,79 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://graph.microsoft.com${resource}`,
json: true
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'microsoftTeamsOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) {
// Try to return the error prettier
throw new Error(`Microsoft error response [${error.statusCode}]: ${error.response.body.error.message}`);
}
throw error;
}
}
export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
do {
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
uri = responseData['@odata.nextLink'];
returnData.push.apply(returnData, responseData[propertyName]);
if (query.limit && query.limit <= returnData.length) {
return returnData;
}
} while (
responseData['@odata.nextLink'] !== undefined
);
return returnData;
}
export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query['$top'] = 100;
query['$skip'] = 0;
do {
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query);
query['$skip'] += query['$top'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['value'].length !== 0
);
return returnData;
}

View file

@ -0,0 +1,217 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
microsoftApiRequest,
microsoftApiRequestAllItems,
} from './GenericFunctions';
import {
channelFields,
channelOperations,
} from './ChannelDescription';
import {
channelMessageFields,
channelMessageOperations,
} from './ChannelMessageDescription';
export class MicrosoftTeams implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft Teams',
name: 'microsoftTeams',
icon: 'file:teams.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Microsoft Teams API',
defaults: {
name: 'Microsoft Teams',
color: '#555cc7',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'microsoftTeamsOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Channel',
value: 'channel',
},
{
name: 'Channel Message (Beta)',
value: 'channelMessage',
},
],
default: 'channel',
description: 'The resource to operate on.',
},
// CHANNEL
...channelOperations,
...channelFields,
/// MESSAGE
...channelMessageOperations,
...channelMessageFields,
],
};
methods = {
loadOptions: {
// Get all the team's channels to display them to user so that he can
// select them easily
async getChannels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const teamId = this.getCurrentNodeParameter('teamId') as string;
const { value } = await microsoftApiRequest.call(this, 'GET', `/v1.0/teams/${teamId}/channels`);
for (const channel of value) {
const channelName = channel.displayName;
const channelId = channel.id;
returnData.push({
name: channelName,
value: channelId,
});
}
return returnData;
},
// Get all the teams to display them to user so that he can
// select them easily
async getTeams(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { value } = await microsoftApiRequest.call(this, 'GET', '/v1.0/me/joinedTeams');
for (const team of value) {
const teamName = team.displayName;
const teamId = team.id;
returnData.push({
name: teamName,
value: teamId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'channel') {
//https://docs.microsoft.com/en-us/graph/api/channel-post?view=graph-rest-beta&tabs=http
if (operation === 'create') {
const teamId = this.getNodeParameter('teamId', i) as string;
const name = this.getNodeParameter('name', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {
displayName: name,
};
if (options.description) {
body.description = options.description as string;
}
if (options.type) {
body.membershipType = options.type as string;
}
responseData = await microsoftApiRequest.call(this, 'POST', `/v1.0/teams/${teamId}/channels`, body);
}
//https://docs.microsoft.com/en-us/graph/api/channel-delete?view=graph-rest-beta&tabs=http
if (operation === 'delete') {
const teamId = this.getNodeParameter('teamId', i) as string;
const channelId = this.getNodeParameter('channelId', i) as string;
responseData = await microsoftApiRequest.call(this, 'DELETE', `/v1.0/teams/${teamId}/channels/${channelId}`);
responseData = { success: true };
}
//https://docs.microsoft.com/en-us/graph/api/channel-get?view=graph-rest-beta&tabs=http
if (operation === 'get') {
const teamId = this.getNodeParameter('teamId', i) as string;
const channelId = this.getNodeParameter('channelId', i) as string;
responseData = await microsoftApiRequest.call(this, 'GET', `/v1.0/teams/${teamId}/channels/${channelId}`);
}
//https://docs.microsoft.com/en-us/graph/api/channel-list?view=graph-rest-beta&tabs=http
if (operation === 'getAll') {
const teamId = this.getNodeParameter('teamId', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/v1.0/teams/${teamId}/channels`);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/v1.0/teams/${teamId}/channels`, {});
responseData = responseData.splice(0, qs.limit);
}
}
//https://docs.microsoft.com/en-us/graph/api/channel-patch?view=graph-rest-beta&tabs=http
if (operation === 'update') {
const teamId = this.getNodeParameter('teamId', i) as string;
const channelId = this.getNodeParameter('channelId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {};
if (updateFields.name) {
body.displayName = updateFields.name as string;
}
if (updateFields.description) {
body.description = updateFields.description as string;
}
responseData = await microsoftApiRequest.call(this, 'PATCH', `/v1.0/teams/${teamId}/channels/${channelId}`, body);
responseData = { success: true };
}
}
if (resource === 'channelMessage') {
//https://docs.microsoft.com/en-us/graph/api/channel-post-messages?view=graph-rest-beta&tabs=http
if (operation === 'create') {
const teamId = this.getNodeParameter('teamId', i) as string;
const channelId = this.getNodeParameter('channelId', i) as string;
const messageType = this.getNodeParameter('messageType', i) as string;
const message = this.getNodeParameter('message', i) as string;
const body: IDataObject = {
body: {
contentType: messageType,
content: message,
}
};
responseData = await microsoftApiRequest.call(this, 'POST', `/beta/teams/${teamId}/channels/${channelId}/messages`, body);
}
//https://docs.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-beta&tabs=http
if (operation === 'getAll') {
const teamId = this.getNodeParameter('teamId', i) as string;
const channelId = this.getNodeParameter('channelId', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/beta/teams/${teamId}/channels/${channelId}/messages`);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await microsoftApiRequestAllItems.call(this, 'value', 'GET', `/beta/teams/${teamId}/channels/${channelId}/messages`, {});
responseData = responseData.splice(0, qs.limit);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -113,6 +113,7 @@
"dist/credentials/MicrosoftOAuth2Api.credentials.js",
"dist/credentials/MicrosoftOneDriveOAuth2Api.credentials.js",
"dist/credentials/MicrosoftSql.credentials.js",
"dist/credentials/MicrosoftTeamsOAuth2Api.credentials.js",
"dist/credentials/MoceanApi.credentials.js",
"dist/credentials/MondayComApi.credentials.js",
"dist/credentials/MongoDb.credentials.js",
@ -289,6 +290,7 @@
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
"dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js",
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
"dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
"dist/nodes/MoveBinaryData.node.js",
"dist/nodes/Mocean/Mocean.node.js",
"dist/nodes/MondayCom/MondayCom.node.js",