MM-34444 Detox/E2E: Prepare plugin API and webhook server (#5257)

* Detox/E2E: Prepare plugin API and webhook server

* Added plugin to index

* Used fork version of client-oauth2
This commit is contained in:
Joseph Baylon 2021-03-30 09:15:50 -07:00 committed by GitHub
parent 64ccb58684
commit d898286ec9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 895 additions and 2 deletions

View file

@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const axios = require('axios');
module.exports = async ({sender, message, channelId, rootId, createAt = 0, baseUrl}) => {
const loginResponse = await axios({
url: `${baseUrl}/api/v4/users/login`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'post',
data: {login_id: sender.username, password: sender.password},
});
const setCookie = loginResponse.headers['set-cookie'];
let cookieString = '';
setCookie.forEach((cookie) => {
const nameAndValue = cookie.split(';')[0];
cookieString += nameAndValue + ';';
});
let response;
try {
response = await axios({
url: `${baseUrl}/api/v4/posts`,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
Cookie: cookieString,
},
method: 'post',
data: {
channel_id: channelId,
message,
type: '',
create_at: createAt,
parent_id: rootId,
root_id: rootId,
},
});
} catch (err) {
if (err.response) {
response = err.response;
}
}
return {status: response.status, data: response.data};
};

View file

@ -3,6 +3,7 @@
import Channel from './channel';
import Ldap from './ldap';
import Plugin from './plugin';
import Post from './post';
import Preference from './preference';
import Setup from './setup';
@ -14,6 +15,7 @@ import User from './user';
export {
Channel,
Ldap,
Plugin,
Post,
Preference,
Setup,

View file

@ -0,0 +1,144 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'path';
import client from './client';
import {apiUploadFile, getResponseFromError} from './common';
// ****************************************************************
// Plugins
// https://api.mattermost.com/#tag/plugins
//
// Exported API function should have the following:
// - documented using JSDoc
// - meaningful description
// - match the referenced API endpoints
// - parameter/s defined by `@param`
// - return value defined by `@return`
// ****************************************************************
/**
* Get plugins.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins/get
* @return {Object} returns {plugins} on success or {error, status} on error
*/
export const apiGetAllPlugins = async () => {
try {
const response = await client.get('/api/v4/plugins');
return {plugins: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Upload plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins/post
* @param {string} filename - the filename of plugin to be uploaded
* @return {Object} returns response on success or {error, status} on error
*/
export const apiUploadPlugin = async (filename) => {
try {
const absFilePath = path.resolve(__dirname, `../../support/fixtures/${filename}`);
const response = await apiUploadFile('plugin', absFilePath, {url: '/api/v4/plugins', method: 'POST'});
return response;
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Install plugin from URL.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1install_from_url/post
* @param {string} pluginDownloadUrl - URL used to download the plugin
* @param {string} force - Set to 'true' to overwrite a previously installed plugin with the same ID, if any
* @return {Object} returns {plugin} on success or {error, status} on error
*/
export const apiInstallPluginFromUrl = async (pluginDownloadUrl, force = false) => {
try {
const response = await client.post(`/api/v4/plugins/install_from_url?plugin_download_url=${encodeURIComponent(pluginDownloadUrl)}&force=${force}`);
return {plugin: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Enable plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~1enable/post
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiEnablePluginById = async (pluginId) => {
try {
const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`);
return response;
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Disable plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~1disable/post
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiDisablePluginById = async (pluginId) => {
try {
const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`);
return response;
} catch (err) {
return getResponseFromError(err);
}
};
const prepackagedPlugins = [
'antivirus',
'mattermost-autolink',
'com.mattermost.aws-sns',
'com.mattermost.plugin-channel-export',
'com.mattermost.custom-attributes',
'github',
'com.github.manland.mattermost-plugin-gitlab',
'com.mattermost.plugin-incident-management',
'jenkins',
'jira',
'com.mattermost.nps',
'com.mattermost.welcomebot',
'zoom',
];
/**
* Disable non-prepackaged plugins.
*/
export const apiDisableNonPrepackagedPlugins = async () => {
const {plugins} = await apiGetAllPlugins();
plugins.active.forEach(async (plugin) => {
if (!prepackagedPlugins.includes(plugin.id)) {
await apiDisablePluginById(plugin.id);
}
});
};
/**
* Remove plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}/delete
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiRemovePluginById = async (pluginId) => {
try {
const response = await client.delete(`/api/v4/plugins/${encodeURIComponent(pluginId)}`);
return response;
} catch (err) {
return getResponseFromError(err);
}
};

View file

@ -181,7 +181,7 @@ export const apiRequireLicenseForFeature = async (key = '') => {
/**
* Upload server license with file expected at "/detox/e2e/support/fixtures/mattermost-license.txt"
* @return {Object} returns response
* @return {Object} returns response on success or {error, status} on error
*/
export const apiUploadLicense = async () => {
const absFilePath = path.resolve(__dirname, '../../support/fixtures/mattermost-license.txt');

View file

@ -0,0 +1,270 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
function getFullDialog(triggerId, webhookBaseUrl) {
return {
trigger_id: triggerId,
url: `${webhookBaseUrl}/dialog_submit`,
dialog: {
callback_id: 'somecallbackid',
title: 'Title for Full Dialog Test',
icon_url:
'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png',
elements: [
{
display_name: 'Display Name',
name: 'realname',
type: 'text',
subtype: '',
default: 'default text',
placeholder: 'placeholder',
help_text:
'This a regular input in an interactive dialog triggered by a test integration.',
optional: false,
min_length: 0,
max_length: 0,
data_source: '',
options: null,
},
{
display_name: 'Email',
name: 'someemail',
type: 'text',
subtype: 'email',
default: '',
placeholder: 'placeholder@bladekick.com',
help_text:
'This a regular email input in an interactive dialog triggered by a test integration.',
optional: false,
min_length: 0,
max_length: 0,
data_source: '',
options: null,
},
{
display_name: 'Number',
name: 'somenumber',
type: 'text',
subtype: 'number',
default: '',
placeholder: '',
help_text: '',
optional: false,
min_length: 0,
max_length: 0,
data_source: '',
options: null,
},
{
display_name: 'Password',
name: 'somepassword',
type: 'text',
subtype: 'password',
default: 'p@ssW0rd',
placeholder: 'placeholder',
help_text:
'This a password input in an interactive dialog triggered by a test integration.',
optional: true,
min_length: 0,
max_length: 0,
data_source: '',
options: null,
},
{
display_name: 'Display Name Long Text Area',
name: 'realnametextarea',
type: 'textarea',
subtype: '',
default: '',
placeholder: 'placeholder',
help_text: '',
optional: true,
min_length: 5,
max_length: 100,
data_source: '',
options: null,
},
{
display_name: 'User Selector',
name: 'someuserselector',
type: 'select',
subtype: '',
default: '',
placeholder: 'Select a user...',
help_text: '',
optional: false,
min_length: 0,
max_length: 0,
data_source: 'users',
options: null,
},
{
display_name: 'Channel Selector',
name: 'somechannelselector',
type: 'select',
subtype: '',
default: '',
placeholder: 'Select a channel...',
help_text: 'Choose a channel from the list.',
optional: true,
min_length: 0,
max_length: 0,
data_source: 'channels',
options: null,
},
{
display_name: 'Option Selector',
name: 'someoptionselector',
type: 'select',
subtype: '',
default: '',
placeholder: 'Select an option...',
help_text: '',
optional: false,
min_length: 0,
max_length: 0,
data_source: '',
options: [
{
text: 'Option1',
value: 'opt1',
},
{
text: 'Option2',
value: 'opt2',
},
{
text: 'Option3',
value: 'opt3',
},
],
},
{
display_name: 'Radio Option Selector',
name: 'someradiooptions',
type: 'radio',
help_text: '',
optional: false,
options: [
{
text: 'Engineering',
value: 'engineering',
},
{
text: 'Sales',
value: 'sales',
},
],
},
{
display_name: 'Boolean Selector',
placeholder: 'Was this modal helpful?',
name: 'boolean_input',
type: 'bool',
default: 'True',
optional: true,
help_text: 'This is the help text',
},
],
submit_label: 'Submit',
notify_on_cancel: true,
state: 'somestate',
},
};
}
function getSimpleDialog(triggerId, webhookBaseUrl) {
return {
trigger_id: triggerId,
url: `${webhookBaseUrl}/dialog_submit`,
dialog: {
callback_id: 'somecallbackid',
title: 'Title for Dialog Test without elements',
icon_url:
'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png',
submit_label: 'Submit Test',
notify_on_cancel: true,
state: 'somestate',
},
};
}
function getUserAndChannelDialog(triggerId, webhookBaseUrl) {
return {
trigger_id: triggerId,
url: `${webhookBaseUrl}/dialog_submit`,
dialog: {
callback_id: 'somecallbackid',
title: 'Title for Dialog Test with user and channel element',
icon_url:
'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png',
submit_label: 'Submit Test',
notify_on_cancel: true,
state: 'somestate',
elements: [
{
display_name: 'User Selector',
name: 'someuserselector',
type: 'select',
subtype: '',
default: '',
placeholder: 'Select a user...',
help_text: '',
optional: false,
min_length: 0,
max_length: 0,
data_source: 'users',
options: null,
},
{
display_name: 'Channel Selector',
name: 'somechannelselector',
type: 'select',
subtype: '',
default: '',
placeholder: 'Select a channel...',
help_text: 'Choose a channel from the list.',
optional: true,
min_length: 0,
max_length: 0,
data_source: 'channels',
options: null,
},
],
},
};
}
function getBooleanDialog(triggerId, webhookBaseUrl) {
return {
trigger_id: triggerId,
url: `${webhookBaseUrl}/dialog_submit`,
dialog: {
callback_id: 'somecallbackid',
title: 'Title for Dialog Test with boolean element',
icon_url:
'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png',
submit_label: 'Submit Test',
notify_on_cancel: true,
state: 'somestate',
elements: [
{
display_name: 'Boolean Selector',
placeholder: 'Was this modal helpful?',
name: 'boolean_input',
type: 'bool',
default: 'True',
optional: true,
help_text: 'This is the help text',
},
],
},
};
}
module.exports = {
getFullDialog,
getSimpleDialog,
getUserAndChannelDialog,
getBooleanDialog,
};

132
detox/package-lock.json generated
View file

@ -3454,6 +3454,12 @@
}
}
},
"@servie/events": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@servie/events/-/events-1.0.0.tgz",
"integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==",
"dev": true
},
"@sinonjs/commons": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
@ -3570,6 +3576,12 @@
"integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
"dev": true
},
"@types/tough-cookie": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.7.tgz",
"integrity": "sha512-rMQbgMGxnLsdn8e9aPVyuN+zMQLrZ2QW8xlv7eWS1mydfGXN+tsTKffcIzd8rGCcLdmi3xvQw2MDaZI1bBNTaw==",
"dev": true
},
"@types/yargs": {
"version": "15.0.9",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz",
@ -4061,6 +4073,12 @@
"exception-formatter": "^1.0.4"
}
},
"byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz",
"integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==",
"dev": true
},
"cache-base": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
@ -4196,6 +4214,23 @@
}
}
},
"client-oauth2": {
"version": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"from": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"dev": true,
"requires": {
"popsicle": "^12.0.5",
"safe-buffer": "^5.2.0"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
}
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -7360,6 +7395,21 @@
}
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"make-error-cause": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-2.3.0.tgz",
"integrity": "sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==",
"dev": true,
"requires": {
"make-error": "^1.3.5"
}
},
"makeerror": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
@ -7907,6 +7957,65 @@
}
}
},
"popsicle": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/popsicle/-/popsicle-12.1.0.tgz",
"integrity": "sha512-muNC/cIrWhfR6HqqhHazkxjob3eyECBe8uZYSQ/N5vixNAgssacVleerXnE8Are5fspR0a+d2qWaBR1g7RYlmw==",
"dev": true,
"requires": {
"popsicle-content-encoding": "^1.0.0",
"popsicle-cookie-jar": "^1.0.0",
"popsicle-redirects": "^1.1.0",
"popsicle-transport-http": "^1.0.8",
"popsicle-transport-xhr": "^2.0.0",
"popsicle-user-agent": "^1.0.0",
"servie": "^4.3.3",
"throwback": "^4.1.0"
}
},
"popsicle-content-encoding": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/popsicle-content-encoding/-/popsicle-content-encoding-1.0.0.tgz",
"integrity": "sha512-4Df+vTfM8wCCJVTzPujiI6eOl3SiWQkcZg0AMrOkD1enMXsF3glIkFUZGvour1Sj7jOWCsNSEhBxpbbhclHhzw==",
"dev": true
},
"popsicle-cookie-jar": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/popsicle-cookie-jar/-/popsicle-cookie-jar-1.0.0.tgz",
"integrity": "sha512-vrlOGvNVELko0+J8NpGC5lHWDGrk8LQJq9nwAMIVEVBfN1Lib3BLxAaLRGDTuUnvl45j5N9dT2H85PULz6IjjQ==",
"dev": true,
"requires": {
"@types/tough-cookie": "^2.3.5",
"tough-cookie": "^3.0.1"
}
},
"popsicle-redirects": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/popsicle-redirects/-/popsicle-redirects-1.1.0.tgz",
"integrity": "sha512-XCpzVjVk7tty+IJnSdqWevmOr1n8HNDhL86v7mZ6T1JIIf2KGybxUk9mm7ZFOhWMkGB0e8XkacHip7BV8AQWQA==",
"dev": true
},
"popsicle-transport-http": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/popsicle-transport-http/-/popsicle-transport-http-1.1.3.tgz",
"integrity": "sha512-NYXgiwBw87vzh/r7Igwa80f6nyYwvMMjLJsL6hnQugIy0sJ7c4JRkUDCja31WHdVukCfMj6s+BVzISYdQHg37g==",
"dev": true,
"requires": {
"make-error-cause": "^2.2.0"
}
},
"popsicle-transport-xhr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/popsicle-transport-xhr/-/popsicle-transport-xhr-2.0.0.tgz",
"integrity": "sha512-5Sbud4Widngf1dodJE5cjEYXkzEUIl8CzyYRYR57t6vpy9a9KPGQX6KBKdPjmBZlR5A06pOBXuJnVr23l27rtA==",
"dev": true
},
"popsicle-user-agent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/popsicle-user-agent/-/popsicle-user-agent-1.0.0.tgz",
"integrity": "sha512-epKaq3TTfTzXcxBxjpoKYMcTTcAX8Rykus6QZu77XNhJuRHSRxMd+JJrbX/3PFI0opFGSN0BabbAYCbGxbu0mA==",
"dev": true
},
"posix-character-classes": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@ -8530,6 +8639,17 @@
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"servie": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/servie/-/servie-4.3.3.tgz",
"integrity": "sha512-b0IrY3b1gVMsWvJppCf19g1p3JSnS0hQi6xu4Hi40CIhf0Lx8pQHcvBL+xunShpmOiQzg1NOia812NAWdSaShw==",
"dev": true,
"requires": {
"@servie/events": "^1.0.0",
"byte-length": "^1.0.2",
"ts-expect": "^1.1.0"
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@ -9068,6 +9188,12 @@
"integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==",
"dev": true
},
"throwback": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz",
"integrity": "sha512-dLFe8bU8SeH0xeqeKL7BNo8XoPC/o91nz9/ooeplZPiso+DZukhoyZcSz9TFnUNScm+cA9qjU1m1853M6sPOng==",
"dev": true
},
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
@ -9150,6 +9276,12 @@
"utf8-byte-length": "^1.0.1"
}
},
"ts-expect": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-expect/-/ts-expect-1.3.0.tgz",
"integrity": "sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==",
"dev": true
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View file

@ -11,6 +11,7 @@
"axios": "0.21.1",
"babel-jest": "26.6.3",
"babel-plugin-module-resolver": "4.1.0",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"deepmerge": "4.2.2",
"detox": "18.6.2",
"form-data": "4.0.0",
@ -30,7 +31,8 @@
"e2e:android-test-release": "detox test -c android.emu.release --record-logs failing --take-screenshots failing",
"e2e:ios-test": "IOS=true detox test -c ios.sim.debug",
"e2e:ios-build-release": "detox build -c ios.sim.release",
"e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing"
"e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing",
"start:webhook": "node webhook_server.js"
},
"jest": {
"transform": {

296
detox/webhook_server.js Normal file
View file

@ -0,0 +1,296 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable camelcase, no-console */
const express = require('express');
const axios = require('axios');
var ClientOAuth2 = require('client-oauth2');
const webhookUtils = require('./e2e/utils/webhook_utils');
const postMessageAs = require('./e2e/plugins/post_message_as');
const port = 3000;
const {
SITE_URL,
WEBHOOK_BASE_URL,
ADMIN_USERNAME,
ADMIN_PASSWORD,
} = process.env; // eslint-disable-line no-process-env
const server = express();
server.use(express.json());
server.use(express.urlencoded({extended: true}));
process.title = process.argv[2];
server.get('/', ping);
server.post('/message_menus', postMessageMenus);
server.post('/dialog_request', onDialogRequest);
server.post('/simple_dialog_request', onSimpleDialogRequest);
server.post('/user_and_channel_dialog_request', onUserAndChannelDialogRequest);
server.post('/dialog_submit', onDialogSubmit);
server.post('/boolean_dialog_request', onBooleanDialogRequest);
server.post('/slack_compatible_message_response', postSlackCompatibleMessageResponse);
server.post('/send_message_to_channel', postSendMessageToChannel);
server.post('/post_outgoing_webhook', postOutgoingWebhook);
server.post('/send_oauth_credentials', postSendOauthCredentials);
server.get('/start_oauth', getStartOAuth);
server.get('/complete_oauth', getCompleteOauth);
server.post('/postOAuthMessage', postOAuthMessage);
function ping(req, res) {
const baseUrl = SITE_URL || 'http://localhost:8065';
const webhookBaseUrl = WEBHOOK_BASE_URL || 'http://localhost:3000';
return res.json({
message: 'I\'m alive!',
baseUrl,
webhookBaseUrl,
});
}
server.listen(port, () => console.log(`Webhook test server listening on port ${port}!`));
let appID;
let appSecret;
let client;
let authedUser;
function postSendOauthCredentials(req, res) {
appID = req.body.appID.trim();
appSecret = req.body.appSecret.trim();
client = new ClientOAuth2({
clientId: appID,
clientSecret: appSecret,
authorizationUri: getBaseUrl() + '/oauth/authorize',
accessTokenUri: getBaseUrl() + '/oauth/access_token',
redirectUri: getWebhookBaseUrl() + '/complete_oauth',
});
return res.status(200).send('OK');
}
function getStartOAuth(req, res) {
return res.redirect(client.code.getUri());
}
function getCompleteOauth(req, res) {
client.code.getToken(req.originalUrl).then((user) => {
authedUser = user;
return res.status(200).send('OK');
}).catch((reason) => {
return res.status(reason.status).send(reason);
});
}
async function postOAuthMessage(req, res) {
const {channelId, message, rootId, createAt} = req.body;
const apiUrl = getBaseUrl() + '/api/v4/posts';
authedUser.sign({
method: 'post',
url: apiUrl,
});
try {
await axios({
url: apiUrl,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
Authorization: 'Bearer ' + authedUser.accessToken,
},
method: 'post',
data: {
channel_id: channelId,
message,
type: '',
create_at: createAt,
parent_id: rootId,
root_id: rootId,
},
});
} catch (err) {
// Do nothing
}
return res.status(200).send('OK');
}
function postSlackCompatibleMessageResponse(req, res) {
const {spoiler, skipSlackParsing} = req.body.context;
res.setHeader('Content-Type', 'application/json');
return res.json({
ephemeral_text: spoiler,
skip_slack_parsing: skipSlackParsing,
});
}
function postMessageMenus(req, res) {
let responseData = {};
const {body} = req;
if (body && body.context.action === 'do_something') {
responseData = {
ephemeral_text: `Ephemeral | ${body.type} ${body.data_source} option: ${body.context.selected_option}`,
};
}
res.setHeader('Content-Type', 'application/json');
return res.json(responseData);
}
async function openDialog(dialog) {
const baseUrl = getBaseUrl();
await axios({
method: 'post',
url: `${baseUrl}/api/v4/actions/dialogs/open`,
data: dialog,
});
}
function onDialogRequest(req, res) {
const {body} = req;
if (body.trigger_id) {
const webhookBaseUrl = getWebhookBaseUrl();
const dialog = webhookUtils.getFullDialog(body.trigger_id, webhookBaseUrl);
openDialog(dialog);
}
res.setHeader('Content-Type', 'application/json');
return res.json({text: 'Full dialog triggered via slash command!'});
}
function onSimpleDialogRequest(req, res) {
const {body} = req;
if (body.trigger_id) {
const webhookBaseUrl = getWebhookBaseUrl();
const dialog = webhookUtils.getSimpleDialog(body.trigger_id, webhookBaseUrl);
openDialog(dialog);
}
res.setHeader('Content-Type', 'application/json');
return res.json({text: 'Simple dialog triggered via slash command!'});
}
function onUserAndChannelDialogRequest(req, res) {
const {body} = req;
if (body.trigger_id) {
const webhookBaseUrl = getWebhookBaseUrl();
const dialog = webhookUtils.getUserAndChannelDialog(body.trigger_id, webhookBaseUrl);
openDialog(dialog);
}
res.setHeader('Content-Type', 'application/json');
return res.json({text: 'Simple dialog triggered via slash command!'});
}
function onBooleanDialogRequest(req, res) {
const {body} = req;
if (body.trigger_id) {
const webhookBaseUrl = getWebhookBaseUrl();
const dialog = webhookUtils.getBooleanDialog(body.trigger_id, webhookBaseUrl);
openDialog(dialog);
}
res.setHeader('Content-Type', 'application/json');
return res.json({text: 'Simple dialog triggered via slash command!'});
}
function onDialogSubmit(req, res) {
const {body} = req;
res.setHeader('Content-Type', 'application/json');
let message;
if (body.cancelled) {
message = 'Dialog cancelled';
sendSysadminResponse(message, body.channel_id);
} else {
message = 'Dialog submitted';
sendSysadminResponse(message, body.channel_id);
}
return res.json({text: message});
}
/**
* @route "POST /send_message_to_channel?type={messageType}&channel_id={channelId}"
* @query type - message type of empty string for regular message if not provided (default), "system_message", etc
* @query channel_id - channel where to send the message
*/
function postSendMessageToChannel(req, res) {
const channelId = req.query.channel_id;
const response = {
response_type: 'in_channel',
text: 'Extra response 2',
channel_id: channelId,
extra_responses: [{
response_type: 'in_channel',
text: 'Hello World',
channel_id: channelId,
}],
};
if (req.query.type) {
response.type = req.query.type;
}
res.json(response);
}
function getWebhookBaseUrl() {
return WEBHOOK_BASE_URL || 'http://localhost:3000';
}
function getBaseUrl() {
return SITE_URL || 'http://localhost:8065';
}
// Convenient way to send response in a channel by using sysadmin account
function sendSysadminResponse(message, channelId) {
const username = ADMIN_USERNAME || 'sysadmin';
const password = ADMIN_PASSWORD || 'Sys@dmin-sample1';
const baseUrl = getBaseUrl();
postMessageAs({sender: {username, password}, message, channelId, baseUrl});
}
const responseTypes = ['in_channel', 'comment'];
function getWebhookResponse(body, {responseType, username, iconUrl}) {
const payload = Object.entries(body).map(([key, value]) => `- ${key}: "${value}"`).join('\n');
return `
\`\`\`
#### Outgoing Webhook Payload
${payload}
#### Webhook override to Mattermost instance
- response_type: "${responseType}"
- type: ""
- username: "${username}"
- icon_url: "${iconUrl}"
\`\`\`
`;
}
/**
* @route "POST /post_outgoing_webhook?override_username={username}&override_icon_url={iconUrl}&response_type={comment}"
* @query override_username - the user name that overrides the user name defined by the outgoing webhook
* @query override_icon_url - the user icon url that overrides the user icon url defined by the outgoing webhook
* @query response_type - "in_channel" (default) or "comment"
*/
function postOutgoingWebhook(req, res) {
const {body, query} = req;
if (!body) {
res.status(404).send({error: 'Invalid data'});
}
const responseType = query.response_type || responseTypes[0];
const username = query.override_username || '';
const iconUrl = query.override_icon_url || '';
const response = {
text: getWebhookResponse(body, {responseType, username, iconUrl}),
username,
icon_url: iconUrl,
type: '',
response_type: responseType,
};
res.status(200).send(response);
}