summaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/react/components/admin_console/service_settings.jsx104
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/user_settings/manage_command_hooks.jsx652
-rw-r--r--web/react/components/user_settings/user_settings_integrations.jsx43
-rw-r--r--web/react/utils/async_client.jsx19
-rw-r--r--web/react/utils/client.jsx76
-rw-r--r--web/static/i18n/en.json45
-rw-r--r--web/static/i18n/es.json4
-rw-r--r--web/web_test.go4
9 files changed, 928 insertions, 26 deletions
diff --git a/web/react/components/admin_console/service_settings.jsx b/web/react/components/admin_console/service_settings.jsx
index 7021900eb..2cc68d1ed 100644
--- a/web/react/components/admin_console/service_settings.jsx
+++ b/web/react/components/admin_console/service_settings.jsx
@@ -75,6 +75,8 @@ class ServiceSettings extends React.Component {
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
+ config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
+ config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
//config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
@@ -389,11 +391,105 @@ class ServiceSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor='EnableCommands'
+ >
+ <FormattedMessage
+ id='admin.service.cmdsTitle'
+ defaultMessage='Enable Slash Commands: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableCommands'
+ value='true'
+ ref='EnableCommands'
+ defaultChecked={this.props.config.ServiceSettings.EnableCommands}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableCommands'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableCommands}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.cmdsDesc'
+ defaultMessage='When true, user created slash commands will be allowed.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='EnableOnlyAdminIntegrations'
+ >
+ <FormattedMessage
+ id='admin.service.integrationAdmin'
+ defaultMessage='Enable Integrations for Admin Only: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOnlyAdminIntegrations'
+ value='true'
+ ref='EnableOnlyAdminIntegrations'
+ defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.true'
+ defaultMessage='true'
+ />
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='EnableOnlyAdminIntegrations'
+ value='false'
+ defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
+ onChange={this.handleChange}
+ />
+ <FormattedMessage
+ id='admin.service.false'
+ defaultMessage='false'
+ />
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.service.integrationAdminDesc'
+ defaultMessage='When true, user created integrations can only be created by admins.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
htmlFor='EnablePostUsernameOverride'
>
<FormattedMessage
id='admin.service.overrideTitle'
- defaultMessage='Enable Overriding Usernames from Webhooks: '
+ defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
/>
</label>
<div className='col-sm-8'>
@@ -427,7 +523,7 @@ class ServiceSettings extends React.Component {
<p className='help-text'>
<FormattedMessage
id='admin.service.overrideDescription'
- defaultMessage='When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
/>
</p>
</div>
@@ -440,7 +536,7 @@ class ServiceSettings extends React.Component {
>
<FormattedMessage
id='admin.service.iconTitle'
- defaultMessage='Enable Overriding Icon from Webhooks: '
+ defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
/>
</label>
<div className='col-sm-8'>
@@ -474,7 +570,7 @@ class ServiceSettings extends React.Component {
<p className='help-text'>
<FormattedMessage
id='admin.service.iconDescription'
- defaultMessage='When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
+ defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
/>
</p>
</div>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index ed672cd34..20892898e 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -155,15 +155,10 @@ class CreatePost extends React.Component {
post.message,
false,
(data) => {
- if (data.response === 'not implemented') {
- this.sendMessage(post);
- return;
- }
-
PostStore.storeDraft(data.channel_id, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
- if (data.goto_location.length > 0) {
+ if (data.goto_location && data.goto_location.length > 0) {
window.location.href = data.goto_location;
}
},
diff --git a/web/react/components/user_settings/manage_command_hooks.jsx b/web/react/components/user_settings/manage_command_hooks.jsx
new file mode 100644
index 000000000..bcf0a6c82
--- /dev/null
+++ b/web/react/components/user_settings/manage_command_hooks.jsx
@@ -0,0 +1,652 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import LoadingScreen from '../loading_screen.jsx';
+
+import * as Client from '../../utils/client.jsx';
+
+import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'mm-intl';
+
+const holders = defineMessages({
+ requestTypePost: {
+ id: 'user.settings.cmds.request_type_post',
+ defaultMessage: 'POST'
+ },
+ requestTypeGet: {
+ id: 'user.settings.cmds.request_type_get',
+ defaultMessage: 'GET'
+ },
+ addDisplayNamePlaceholder: {
+ id: 'user.settings.cmds.add_display_name.placeholder',
+ defaultMessage: 'Display Name'
+ },
+ addUsernamePlaceholder: {
+ id: 'user.settings.cmds.add_username.placeholder',
+ defaultMessage: 'Username'
+ },
+ addTriggerPlaceholder: {
+ id: 'user.settings.cmds.add_trigger.placeholder',
+ defaultMessage: 'Command trigger e.g. "hello" not including the slash'
+ },
+ addAutoCompleteDescPlaceholder: {
+ id: 'user.settings.cmds.auto_complete_desc.placeholder',
+ defaultMessage: 'A short description of what this commands does.'
+ },
+ addAutoCompleteHintPlaceholder: {
+ id: 'user.settings.cmds.auto_complete_hint.placeholder',
+ defaultMessage: '[zipcode]'
+ },
+ adUrlPlaceholder: {
+ id: 'user.settings.cmds.url.placeholder',
+ defaultMessage: 'Must start with http:// or https://'
+ }
+});
+
+export default class ManageCommandCmds extends React.Component {
+ constructor() {
+ super();
+
+ this.getCmds = this.getCmds.bind(this);
+ this.addNewCmd = this.addNewCmd.bind(this);
+ this.emptyCmd = this.emptyCmd.bind(this);
+ this.updateTrigger = this.updateTrigger.bind(this);
+ this.updateURL = this.updateURL.bind(this);
+ this.updateMethod = this.updateMethod.bind(this);
+ this.updateUsername = this.updateUsername.bind(this);
+ this.updateIconURL = this.updateIconURL.bind(this);
+ this.updateDisplayName = this.updateDisplayName.bind(this);
+ this.updateAutoComplete = this.updateAutoComplete.bind(this);
+ this.updateAutoCompleteDesc = this.updateAutoCompleteDesc.bind(this);
+ this.updateAutoCompleteHint = this.updateAutoCompleteHint.bind(this);
+
+ this.state = {cmds: [], cmd: this.emptyCmd(), getCmdsComplete: false};
+ }
+
+ static propTypes() {
+ return {
+ intl: intlShape.isRequired
+ };
+ }
+
+ emptyCmd() {
+ var cmd = {};
+ cmd.url = '';
+ cmd.trigger = '';
+ cmd.method = 'P';
+ cmd.username = '';
+ cmd.icon_url = '';
+ cmd.auto_complete = false;
+ cmd.auto_complete_desc = '';
+ cmd.auto_complete_hint = '';
+ cmd.display_name = '';
+ return cmd;
+ }
+
+ componentDidMount() {
+ this.getCmds();
+ }
+
+ addNewCmd(e) {
+ e.preventDefault();
+
+ if (this.state.cmd.trigger === '' || this.state.cmd.url === '') {
+ return;
+ }
+
+ var cmd = this.state.cmd;
+ if (cmd.trigger.length !== 0) {
+ cmd.trigger = cmd.trigger.trim();
+ }
+ cmd.url = cmd.url.trim();
+
+ Client.addCommand(
+ cmd,
+ (data) => {
+ let cmds = Object.assign([], this.state.cmds);
+ if (!cmds) {
+ cmds = [];
+ }
+ cmds.push(data);
+ this.setState({cmds, addError: null, cmd: this.emptyCmd()});
+ },
+ (err) => {
+ this.setState({addError: err.message});
+ }
+ );
+ }
+
+ removeCmd(id) {
+ const data = {};
+ data.id = id;
+
+ Client.deleteCommand(
+ data,
+ () => {
+ const cmds = this.state.cmds;
+ let index = -1;
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ index = i;
+ break;
+ }
+ }
+
+ if (index !== -1) {
+ cmds.splice(index, 1);
+ }
+
+ this.setState({cmds});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ regenToken(id) {
+ const regenData = {};
+ regenData.id = id;
+
+ Client.regenCommandToken(
+ regenData,
+ (data) => {
+ const cmds = Object.assign([], this.state.cmds);
+ for (let i = 0; i < cmds.length; i++) {
+ if (cmds[i].id === id) {
+ cmds[i] = data;
+ break;
+ }
+ }
+
+ this.setState({cmds, editError: null});
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ getCmds() {
+ Client.listTeamCommands(
+ (data) => {
+ if (data) {
+ this.setState({cmds: data, getCmdsComplete: true, editError: null});
+ }
+ },
+ (err) => {
+ this.setState({editError: err.message});
+ }
+ );
+ }
+
+ updateTrigger(e) {
+ var cmd = this.state.cmd;
+ cmd.trigger = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateURL(e) {
+ var cmd = this.state.cmd;
+ cmd.url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateMethod(e) {
+ var cmd = this.state.cmd;
+ cmd.method = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateUsername(e) {
+ var cmd = this.state.cmd;
+ cmd.username = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateIconURL(e) {
+ var cmd = this.state.cmd;
+ cmd.icon_url = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateDisplayName(e) {
+ var cmd = this.state.cmd;
+ cmd.display_name = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoComplete(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete = e.target.checked;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteDesc(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_desc = e.target.value;
+ this.setState(cmd);
+ }
+
+ updateAutoCompleteHint(e) {
+ var cmd = this.state.cmd;
+ cmd.auto_complete_hint = e.target.value;
+ this.setState(cmd);
+ }
+
+ render() {
+ let addError;
+ if (this.state.addError) {
+ addError = <label className='has-error'>{this.state.addError}</label>;
+ }
+
+ let editError;
+ if (this.state.editError) {
+ addError = <label className='has-error'>{this.state.editError}</label>;
+ }
+
+ const cmds = [];
+ this.state.cmds.forEach((cmd) => {
+ let triggerDiv;
+ if (cmd.trigger && cmd.trigger.length !== 0) {
+ triggerDiv = (
+ <div className='padding-top'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.trigger'
+ defaultMessage='Trigger: '
+ />
+ </strong>{cmd.trigger}
+ </div>
+ );
+ }
+
+ cmds.push(
+ <div
+ key={cmd.id}
+ className='webcmd__item'
+ >
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.display_name'
+ defaultMessage='Display Name: '
+ />
+ </strong><span className='word-break--all'>{cmd.display_name}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.username'
+ defaultMessage='Username: '
+ />
+ </strong><span className='word-break--all'>{cmd.username}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url'
+ defaultMessage='Icon URL: '
+ />
+ </strong><span className='word-break--all'>{cmd.icon_url}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete'
+ defaultMessage='Auto Complete: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete ? 'yes' : 'no'}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc'
+ defaultMessage='Auto Complete Description: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete_desc}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint'
+ defaultMessage='Auto Complete Hint: '
+ />
+ </strong><span className='word-break--all'>{cmd.auto_complete_hint}</span>
+ </div>
+ <div className='padding-top x2'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.request_type'
+ defaultMessage='Request Type: '
+ />
+ </strong>
+ <span className='word-break--all'>
+ {
+ cmd.method === 'P' ?
+ <FormattedMessage
+ id='user.settings.cmds.request_type_post'
+ defaultMessage='POST'
+ /> :
+ <FormattedMessage
+ id='user.settings.cmds.request_type_get'
+ defaultMessage='GET'
+ />
+ }
+ </span>
+ </div>
+ <div className='padding-top x2 webcmd__url'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.url'
+ defaultMessage='URL: '
+ />
+ </strong><span className='word-break--all'>{cmd.url}</span>
+ </div>
+ {triggerDiv}
+ <div className='padding-top'>
+ <strong>
+ <FormattedMessage
+ id='user.settings.cmds.token'
+ defaultMessage='Token: '
+ />
+ </strong>{cmd.token}
+ </div>
+ <div className='padding-top'>
+ <a
+ className='text-danger'
+ href='#'
+ onClick={this.regenToken.bind(this, cmd.id)}
+ >
+ <FormattedMessage
+ id='user.settings.cmds.regen'
+ defaultMessage='Regen Token'
+ />
+ </a>
+ <a
+ className='webcmd__remove'
+ href='#'
+ onClick={this.removeCmd.bind(this, cmd.id)}
+ >
+ <span aria-hidden='true'>{'×'}</span>
+ </a>
+ </div>
+ <div className='padding-top x2 divider-light'></div>
+ </div>
+ );
+ });
+
+ let displayCmds;
+ if (!this.state.getCmdsComplete) {
+ displayCmds = <LoadingScreen/>;
+ } else if (cmds.length > 0) {
+ displayCmds = cmds;
+ } else {
+ displayCmds = (
+ <div className='padding-top x2'>
+ <FormattedMessage
+ id='user.settings.cmds.none'
+ defaultMessage='None'
+ />
+ </div>
+ );
+ }
+
+ const existingCmds = (
+ <div className='webcmds__container'>
+ <label className='control-label padding-top x2'>
+ <FormattedMessage
+ id='user.settings.cmds.existing'
+ defaultMessage='Existing commands'
+ />
+ </label>
+ <div className='padding-top divider-light'></div>
+ <div className='webcmds__list'>
+ {displayCmds}
+ </div>
+ </div>
+ );
+
+ const disableButton = this.state.cmd.trigger === '' || this.state.cmd.url === '';
+
+ return (
+ <div key='addCommandCmd'>
+ <FormattedHTMLMessage
+ id='user.settings.cmds.add_desc'
+ defaultMessage='Create commands to send message events to an external integration. Please see <a href="http://mattermost.org/commands">http://mattermost.org/commands</a> to learn more.'
+ />
+ <div><label className='control-label padding-top x2'>{'Add a new command'}</label></div>
+ <div className='padding-top divider-light'></div>
+ <div className='padding-top'>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.display_name'
+ defaultMessage='Display Name: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='displayName'
+ className='form-control'
+ value={this.state.cmd.display_name}
+ onChange={this.updateDisplayName}
+ placeholder={this.props.intl.formatMessage(holders.addDisplayNamePlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>{'Command display name.'}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.username'
+ defaultMessage='Username: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='username'
+ className='form-control'
+ value={this.state.cmd.username}
+ onChange={this.updateUsername}
+ placeholder={this.props.intl.formatMessage(holders.addUsernamePlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.username_desc'
+ defaultMessage='The username to use when overriding the post.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url'
+ defaultMessage='Icon URL: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='iconURL'
+ className='form-control'
+ value={this.state.cmd.icon_url}
+ onChange={this.updateIconURL}
+ placeholder='https://www.example.com/myicon.png'
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.icon_url_desc'
+ defaultMessage='URL to an icon'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.trigger'
+ defaultMessage='Trigger: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='trigger'
+ className='form-control'
+ value={this.state.cmd.trigger}
+ onChange={this.updateTrigger}
+ placeholder={this.props.intl.formatMessage(holders.addTriggerPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.trigger_desc'
+ defaultMessage='Word to trigger on'
+ />
+ {''}</div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete'
+ defaultMessage='Auto Complete: '
+ />
+ </label>
+ <div className='padding-top'>
+ <label>
+ <input
+ type='checkbox'
+ checked={this.state.cmd.auto_complete}
+ onChange={this.updateAutoComplete}
+ />
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc_desc'
+ defaultMessage='A short description of what this commands does'
+ />
+ </label>
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_help'
+ defaultMessage='Show this command in autocomplete list.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc'
+ defaultMessage='Auto Complete Description: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteDesc'
+ className='form-control'
+ value={this.state.cmd.auto_complete_desc}
+ onChange={this.updateAutoCompleteDesc}
+ placeholder={this.props.intl.formatMessage(holders.addAutoCompleteDescPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_desc_desc'
+ defaultMessage='A short description of what this commands does'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint'
+ defaultMessage='Auto Complete Hint: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='autoCompleteHint'
+ className='form-control'
+ value={this.state.cmd.auto_complete_hint}
+ onChange={this.updateAutoCompleteHint}
+ placeholder={this.props.intl.formatMessage(holders.addAutoCompleteHintPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.auto_complete_hint_desc'
+ defaultMessage='List parameters to be passed to the command.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.request_type'
+ defaultMessage='Request Type: '
+ />
+ </label>
+ <div className='padding-top'>
+ <select
+ ref='method'
+ className='form-control'
+ value={this.state.cmd.method}
+ onChange={this.updateMethod}
+ >
+ <option value='P'>
+ {this.props.intl.formatMessage(holders.requestTypePost)}
+ </option>
+ <option value='G'>
+ {this.props.intl.formatMessage(holders.requestTypeGet)}
+ </option>
+ </select>
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.request_type_desc'
+ defaultMessage='Command request type issued to the callback URL.'
+ />
+ </div>
+ </div>
+ <div className='padding-top x2'>
+ <label className='control-label'>
+ <FormattedMessage
+ id='user.settings.cmds.url'
+ defaultMessage='URL: '
+ />
+ </label>
+ <div className='padding-top'>
+ <input
+ ref='URL'
+ className='form-control'
+ value={this.state.cmd.url}
+ rows={1}
+ onChange={this.updateURL}
+ placeholder={this.props.intl.formatMessage(holders.adUrlPlaceholder)}
+ />
+ </div>
+ <div className='padding-top'>
+ <FormattedMessage
+ id='user.settings.cmds.url_desc'
+ defaultMessage='URL that will receive the HTTP POST or GET event'
+ />
+ </div>
+ {addError}
+ </div>
+ <div className='padding-top padding-bottom'>
+ <a
+ className={'btn btn-sm btn-primary'}
+ href='#'
+ disabled={disableButton}
+ onClick={this.addNewCmd}
+ >
+ {'Add'}
+ </a>
+ </div>
+ </div>
+ {existingCmds}
+ {editError}
+ </div>
+ );
+ }
+}
+
+export default injectIntl(ManageCommandCmds);
diff --git a/web/react/components/user_settings/user_settings_integrations.jsx b/web/react/components/user_settings/user_settings_integrations.jsx
index abd04a301..1a9edab03 100644
--- a/web/react/components/user_settings/user_settings_integrations.jsx
+++ b/web/react/components/user_settings/user_settings_integrations.jsx
@@ -5,6 +5,7 @@ import SettingItemMin from '../setting_item_min.jsx';
import SettingItemMax from '../setting_item_max.jsx';
import ManageIncomingHooks from './manage_incoming_hooks.jsx';
import ManageOutgoingHooks from './manage_outgoing_hooks.jsx';
+import ManageCommandHooks from './manage_command_hooks.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'mm-intl';
@@ -24,6 +25,14 @@ const holders = defineMessages({
outDesc: {
id: 'user.settings.integrations.outWebhooksDescription',
defaultMessage: 'Manage your outgoing webhooks'
+ },
+ cmdName: {
+ id: 'user.settings.integrations.commands',
+ defaultMessage: 'Commands'
+ },
+ cmdDesc: {
+ id: 'user.settings.integrations.commandsDescription',
+ defaultMessage: 'Manage your commands'
}
});
@@ -41,6 +50,7 @@ class UserSettingsIntegrationsTab extends React.Component {
render() {
let incomingHooksSection;
let outgoingHooksSection;
+ let commandHooksSection;
var inputs = [];
const {formatMessage} = this.props.intl;
@@ -106,6 +116,37 @@ class UserSettingsIntegrationsTab extends React.Component {
}
}
+ if (global.window.mm_config.EnableCommands === 'true') {
+ if (this.props.activeSection === 'command-hooks') {
+ inputs.push(
+ <ManageCommandHooks key='command-hook-ui' />
+ );
+
+ commandHooksSection = (
+ <SettingItemMax
+ title={formatMessage(holders.cmdName)}
+ width='medium'
+ inputs={inputs}
+ updateSection={(e) => {
+ this.updateSection('');
+ e.preventDefault();
+ }}
+ />
+ );
+ } else {
+ commandHooksSection = (
+ <SettingItemMin
+ title={formatMessage(holders.cmdName)}
+ width='medium'
+ describe={formatMessage(holders.cmdDesc)}
+ updateSection={() => {
+ this.updateSection('command-hooks');
+ }}
+ />
+ );
+ }
+ }
+
return (
<div>
<div className='modal-header'>
@@ -144,6 +185,8 @@ class UserSettingsIntegrationsTab extends React.Component {
<div className='divider-light'/>
{outgoingHooksSection}
<div className='divider-dark'/>
+ {commandHooksSection}
+ <div className='divider-dark'/>
</div>
</div>
);
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index d615e02c7..328a7a7f2 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -774,20 +774,27 @@ export function savePreferences(preferences, success, error) {
}
export function getSuggestedCommands(command, suggestionId, component) {
- client.executeCommand(
- '',
- command,
- true,
+ client.listCommands(
(data) => {
+ var matches = [];
+ data.forEach((cmd) => {
+ if (('/' + cmd.trigger).indexOf(command) === 0) {
+ matches.push({
+ suggestion: '/' + cmd.trigger + ' ' + cmd.auto_complete_hint,
+ description: cmd.auto_complete_desc
+ });
+ }
+ });
+
// pull out the suggested commands from the returned data
- const terms = data.suggestions.map((suggestion) => suggestion.suggestion);
+ const terms = matches.map((suggestion) => suggestion.suggestion);
AppDispatcher.handleServerAction({
type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
id: suggestionId,
matchedPretext: command,
terms,
- items: data.suggestions,
+ items: matches,
component
});
},
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 33eb4cd47..992337671 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -908,11 +908,11 @@ export function getChannelExtraInfo(id, memberLimit, success, error) {
export function executeCommand(channelId, command, suggest, success, error) {
$.ajax({
- url: '/api/v1/command',
+ url: '/api/v1/commands/execute',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
- data: JSON.stringify({channelId: channelId, command: command, suggest: '' + suggest}),
+ data: JSON.stringify({channelId, command, suggest: '' + suggest}),
success,
error: function onError(xhr, status, err) {
var e = handleError('executeCommand', xhr, status, err);
@@ -921,6 +921,78 @@ export function executeCommand(channelId, command, suggest, success, error) {
});
}
+export function addCommand(cmd, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/create',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(cmd),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('addCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function deleteCommand(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/delete',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('deleteCommand', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listTeamCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list_team_commands',
+ dataType: 'json',
+ type: 'GET',
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('listTeamCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function regenCommandToken(data, success, error) {
+ $.ajax({
+ url: '/api/v1/commands/regen_token',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify(data),
+ success,
+ error: (xhr, status, err) => {
+ var e = handleError('regenCommandToken', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
+export function listCommands(success, error) {
+ $.ajax({
+ url: '/api/v1/commands/list',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('listCommands', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getPostsPage(channelId, offset, limit, success, error, complete) {
$.ajax({
cache: false,
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index aaffc6ea7..a160aa58f 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -351,10 +351,14 @@
"admin.service.webhooksDescription": "When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.",
"admin.service.outWebhooksTitle": "Enable Outgoing Webhooks: ",
"admin.service.outWebhooksDesc": "When true, outgoing webhooks will be allowed.",
- "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks: ",
- "admin.service.overrideDescription": "When true, webhooks will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
- "admin.service.iconTitle": "Enable Overriding Icon from Webhooks: ",
- "admin.service.iconDescription": "When true, webhooks will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.",
+ "admin.service.cmdsTitle": "Enable Slash Commands: ",
+ "admin.service.cmdsDesc": "When true, user created slash commands will be allowed.",
+ "admin.service.integrationAdmin": "Enable Integrations for Admin Only: ",
+ "admin.service.integrationAdminDesc": "When true, user created integrations can only be created by admins.",
+ "admin.service.overrideTitle": "Enable Overriding Usernames from Webhooks and Salsh Commands: ",
+ "admin.service.overrideDescription": "When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.",
+ "admin.service.iconTitle": "Enable Overriding Icon from Webhooks and Slash Commands: ",
+ "admin.service.iconDescription": "When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.",
"admin.service.testingTitle": "Enable Testing: ",
"admin.service.testingDescription": "(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.",
"admin.service.developerTitle": "Enable Developer Mode: ",
@@ -1148,7 +1152,40 @@
"user.settings.integrations.incomingWebhooksDescription": "Manage your incoming webhooks",
"user.settings.integrations.outWebhooks": "Outgoing Webhooks",
"user.settings.integrations.outWebhooksDescription": "Manage your outgoing webhooks",
+ "user.settings.integrations.commands": "Commands",
+ "user.settings.integrations.commandsDescription": "Manage your commands",
"user.settings.integrations.title": "Integration Settings",
+ "user.settings.cmds.trigger": "Trigger: ",
+ "user.settings.cmds.display_name": "Display Name: ",
+ "user.settings.cmds.username": "Username: ",
+ "user.settings.cmds.icon_url": "Icon URL: ",
+ "user.settings.cmds.auto_complete": "Auto Complete: ",
+ "user.settings.cmds.auto_complete_desc": "Auto Complete Description: ",
+ "user.settings.cmds.auto_complete_hint": "Auto Complete Hint: ",
+ "user.settings.cmds.request_type": "Request Type: ",
+ "user.settings.cmds.request_type_post": "POST",
+ "user.settings.cmds.request_type_get": "GET",
+ "user.settings.cmds.url": "URL: ",
+ "user.settings.cmds.token": "Token: ",
+ "user.settings.cmds.regen": "Regen Token",
+ "user.settings.cmds.none": "None",
+ "Existing commands": "Existing commands",
+ "user.settings.cmds.add_desc": "Create commands to send message events to an external integration. Please see <a href=\"http://mattermost.org/commands\">http://mattermost.org/commands</a> to learn more.",
+ "user.settings.cmds.add_display_name.placeholder": "Display Name",
+ "user.settings.cmds.existing": "Existing commands",
+ "user.settings.cmds.add_username.placeholder": "Username",
+ "user.settings.cmds.username_desc": "The username to use when overriding the post.",
+ "user.settings.cmds.icon_url_desc": "URL to an icon",
+ "user.settings.cmds.trigger_desc": "Word to trigger on",
+ "user.settings.cmds.add_trigger.placeholder": "Command trigger e.g. \"hello\" not including the slash",
+ "user.settings.cmds.auto_complete_desc_desc": "A short description of what this commands does",
+ "user.settings.cmds.auto_complete_help": "Show this command in autocomplete list.",
+ "user.settings.cmds.auto_complete_desc.placeholder": "A short description of what this commands does.",
+ "user.settings.cmds.auto_complete_hint.placeholder": "[zipcode]",
+ "user.settings.cmds.auto_complete_hint_desc": "List parameters to be passed to the command.",
+ "user.settings.cmds.request_type_desc": "Command request type issued to the callback URL.",
+ "user.settings.cmds.url_desc": "URL that will receive the HTTP POST or GET event",
+ "user.settings.cmds.url.placeholder": "Must start with http:// or https://",
"user.settings.modal.general": "General",
"user.settings.modal.security": "Security",
"user.settings.modal.notifications": "Notifications",
diff --git a/web/static/i18n/es.json b/web/static/i18n/es.json
index 981e1f06f..0a780c442 100644
--- a/web/static/i18n/es.json
+++ b/web/static/i18n/es.json
@@ -299,8 +299,6 @@
"admin.service.googleDescription": "Asigna una llave a este campo para habilitar la previsualización de videos de YouTube tomados de los enlaces que aparecen en los mensajes o comentarios. Las instrucciones de como obtener una llave está disponible en <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Al dejar este campo en blanco deshabilita la generación de previsualizaciones de videos de YouTube desde los enlaces.",
"admin.service.googleExample": "Ej \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Llave de desarrolador Google:",
- "admin.service.iconDescription": "Cuando es verdadero, se le permitirá cambiar el icono del mensaje desde webhooks. Nota, en combinación con permitir el cambio de nombre de usuario, podría exponer a los usuarios a sufrir ataques de phishing.",
- "admin.service.iconTitle": "Habilitar el cambio de icono desde los Webhooks: ",
"admin.service.listenAddress": "Dirección de escucha:",
"admin.service.listenDescription": "La dirección a la que se unirá y escuchará. Ingresar \":8065\" se podrá unir a todas las interfaces o podrá seleccionar una como ej: \"127.0.0.1:8065\". Cambiando este valor es necesario reiniciar el servidor.",
"admin.service.listenExample": "Ej \":8065\"",
@@ -308,8 +306,6 @@
"admin.service.mobileSessionDaysDesc": "La sesión nativa de los dispositivos moviles expirará luego de transcurrido el numero de días especificado y se solicitará al usuario que inicie sesión nuevamente.",
"admin.service.outWebhooksDesc": "Cuando es verdadero, los webhooks de salida serán permitidos.",
"admin.service.outWebhooksTitle": "Habilitar Webhooks de Salida: ",
- "admin.service.overrideDescription": "Cuando es verdadero, se le permitirá cambiar el nombre de usuario desde webhooks. Nota, en conjunto con cambio de icono, podría exponer a los usuarios a sufrir ataques de phishing.",
- "admin.service.overrideTitle": "Habilitar el cambio de nombres de usuario desde los Webhooks: ",
"admin.service.save": "Guardar",
"admin.service.saving": "Guardando....",
"admin.service.securityDesc": "Cuando es verdadero, Los Administradores del Sistema serán notificados por correo electrónico se han anunciado alertas de seguridad relevantes en las últimas 12 horas. Requiere que los correos estén habilitados.",
diff --git a/web/web_test.go b/web/web_test.go
index cc7d22559..7617ae54a 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -194,6 +194,10 @@ func TestIncomingWebhook(t *testing.T) {
user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User)
store.Must(api.Srv.Store.User().VerifyEmail(user.Id))
+ c := &api.Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ api.UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
ApiClient.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}