Total overhaul of how commands are executed (server-side), fixes #387
This commit is contained in:
@ -850,6 +850,74 @@ class IrisCore(pykka.ThreadingActor):
|
||||
else:
|
||||
return response
|
||||
|
||||
def run_command(self, *args, **kwargs):
|
||||
callback = kwargs.get('callback', False)
|
||||
data = kwargs.get('data', {})
|
||||
error = False
|
||||
|
||||
if str(data['id']) not in self.commands:
|
||||
error = {
|
||||
'message': 'Command failed',
|
||||
'description': 'Could not find command by ID "'+str(data['id'])+'"'
|
||||
}
|
||||
else:
|
||||
command = self.commands[str(data['id'])]
|
||||
|
||||
if "method" not in command:
|
||||
error = {
|
||||
'message': 'Command failed',
|
||||
'description': 'Missing required property "method"'
|
||||
}
|
||||
if "url" not in command:
|
||||
error = {
|
||||
'message': 'Command failed',
|
||||
'description': 'Missing required property "url"'
|
||||
}
|
||||
|
||||
if error:
|
||||
if (callback):
|
||||
callback(False, error)
|
||||
return
|
||||
else:
|
||||
return error
|
||||
|
||||
# Construct the request
|
||||
http_client = tornado.httpclient.HTTPClient()
|
||||
if (command['method'] == 'POST'):
|
||||
request = tornado.httpclient.HTTPRequest(command['url'], connect_timeout=5, method='POST', body=json.dumps(command['post_data']), validate_cert=False)
|
||||
else:
|
||||
request = tornado.httpclient.HTTPRequest(command['url'], connect_timeout=5, validate_cert=False)
|
||||
|
||||
# Make the request, and handle any request errors
|
||||
try:
|
||||
command_response = http_client.fetch(request)
|
||||
except Exception as e:
|
||||
error = {
|
||||
'message': 'Command failed',
|
||||
'description': str(e)
|
||||
}
|
||||
if (callback):
|
||||
callback(False, error)
|
||||
return
|
||||
else:
|
||||
return error
|
||||
|
||||
# Attempt to parse JSON
|
||||
try:
|
||||
command_response_body = json.loads(command_response.body)
|
||||
except:
|
||||
command_response_body = command_response.body
|
||||
|
||||
# Finally, return the result
|
||||
response = {
|
||||
'message': 'Command run',
|
||||
'response': command_response_body
|
||||
}
|
||||
if (callback):
|
||||
callback(response)
|
||||
return
|
||||
else:
|
||||
return response
|
||||
|
||||
##
|
||||
# Spotify authentication
|
||||
|
||||
@ -5753,6 +5753,12 @@ function handleException(message) {
|
||||
var description = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
||||
var show_notification = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
|
||||
|
||||
if (!message && data.message) {
|
||||
message = data.message;
|
||||
}
|
||||
if (!description && data.description) {
|
||||
description = data.description;
|
||||
}
|
||||
return {
|
||||
type: 'HANDLE_EXCEPTION',
|
||||
message: message,
|
||||
@ -6442,7 +6448,7 @@ exports.addQueueMetadata = addQueueMetadata;
|
||||
exports.getCommands = getCommands;
|
||||
exports.setCommand = setCommand;
|
||||
exports.removeCommand = removeCommand;
|
||||
exports.sendCommand = sendCommand;
|
||||
exports.runCommand = runCommand;
|
||||
exports.commandsUpdated = commandsUpdated;
|
||||
|
||||
/**
|
||||
@ -6679,11 +6685,11 @@ function removeCommand(id) {
|
||||
};
|
||||
}
|
||||
|
||||
function sendCommand(id) {
|
||||
function runCommand(id) {
|
||||
var notify = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
|
||||
return {
|
||||
type: 'PUSHER_SEND_COMMAND',
|
||||
type: 'PUSHER_RUN_COMMAND',
|
||||
id: id,
|
||||
notify: notify
|
||||
};
|
||||
@ -56860,7 +56866,7 @@ exports.default = UIMiddleware;
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
/* WEBPACK VAR INJECTION */(function($) {
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
@ -57293,38 +57299,23 @@ var PusherMiddleware = function () {
|
||||
next(action);
|
||||
break;
|
||||
|
||||
case 'PUSHER_SEND_COMMAND':
|
||||
case 'PUSHER_RUN_COMMAND':
|
||||
var command = Object.assign({}, pusher.commands[action.id]);
|
||||
var notification_key = 'command_' + action.id;
|
||||
|
||||
if (action.notify) {
|
||||
store.dispatch(uiActions.startProcess(notification_key, 'Sending command'));
|
||||
store.dispatch(uiActions.startProcess(notification_key, 'Running command'));
|
||||
}
|
||||
|
||||
try {
|
||||
var ajax_settings = JSON.parse(command.command);
|
||||
} catch (error) {
|
||||
store.dispatch(uiActions.createNotification({ key: notification_key, type: 'bad', content: 'Command failed', description: error }));
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle success and failure
|
||||
ajax_settings.success = function (response) {
|
||||
console.log("Command sent, response was:", response);
|
||||
|
||||
request(store, 'run_command', { id: action.id }).then(function (response) {
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
if (action.notify) {
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
store.dispatch(uiActions.createNotification({ key: notification_key, type: 'info', content: 'Command sent' }));
|
||||
}
|
||||
};
|
||||
ajax_settings.error = function (xhr, status, error) {
|
||||
console.error("Command failed, response was:", xhr, error);
|
||||
}, function (error) {
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
store.dispatch(uiActions.createNotification({ key: notification_key, type: 'bad', content: 'Command failed', description: xhr.status + ": " + error }));
|
||||
};
|
||||
|
||||
// Actually send the request
|
||||
$.ajax(ajax_settings);
|
||||
store.dispatch(coreActions.handleException('Could not run command', error));
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
@ -57542,7 +57533,6 @@ var PusherMiddleware = function () {
|
||||
}();
|
||||
|
||||
exports.default = PusherMiddleware;
|
||||
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(18)))
|
||||
|
||||
/***/ }),
|
||||
/* 161 */
|
||||
@ -66198,7 +66188,7 @@ var OutputControl = function (_React$Component) {
|
||||
key: command.id,
|
||||
className: 'commands__item commands__item--interactive',
|
||||
onClick: function onClick(e) {
|
||||
return _this2.props.pusherActions.sendCommand(command.id);
|
||||
return _this2.props.pusherActions.runCommand(command.id);
|
||||
} },
|
||||
_react2.default.createElement(_Icon2.default, { className: 'commands__item__icon', name: command.icon }),
|
||||
_react2.default.createElement('span', { className: command.colour + '-background commands__item__background' })
|
||||
@ -73410,7 +73400,7 @@ var Settings = function (_React$Component) {
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'commands__item commands__item--interactive', onClick: function onClick(e) {
|
||||
return _this2.props.pusherActions.sendCommand(command.id, true);
|
||||
return _this2.props.pusherActions.runCommand(command.id, true);
|
||||
} },
|
||||
_react2.default.createElement(_Icon2.default, { className: 'commands__item__icon', name: command.icon }),
|
||||
_react2.default.createElement('span', { className: command.colour + '-background commands__item__background' })
|
||||
@ -73418,7 +73408,11 @@ var Settings = function (_React$Component) {
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'commands-setup__item__url commands__item__url' },
|
||||
command.command && command.command.url ? command.command.url : "-"
|
||||
command.name ? command.name : _react2.default.createElement(
|
||||
'span',
|
||||
{ className: 'grey-text' },
|
||||
command.url
|
||||
)
|
||||
)
|
||||
),
|
||||
_react2.default.createElement(
|
||||
@ -87665,6 +87659,10 @@ var _IconField = __webpack_require__(262);
|
||||
|
||||
var _IconField2 = _interopRequireDefault(_IconField);
|
||||
|
||||
var _TextField = __webpack_require__(220);
|
||||
|
||||
var _TextField2 = _interopRequireDefault(_TextField);
|
||||
|
||||
var _actions = __webpack_require__(13);
|
||||
|
||||
var pusherActions = _interopRequireWildcard(_actions);
|
||||
@ -87698,8 +87696,11 @@ var EditCommand = function (_React$Component) {
|
||||
_this.state = {
|
||||
id: helpers.generateGuid(),
|
||||
icon: 'power_settings_new',
|
||||
name: '',
|
||||
colour: '',
|
||||
command: '{"url":"https://' + window.location.hostname + '/broadlink/sendCommand/power/"}'
|
||||
url: "https://" + window.location.hostname + "/broadlink/sendCommand/power/",
|
||||
method: 'GET',
|
||||
post_data: ""
|
||||
};
|
||||
return _this;
|
||||
}
|
||||
@ -87719,12 +87720,7 @@ var EditCommand = function (_React$Component) {
|
||||
value: function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.pusherActions.setCommand({
|
||||
id: this.state.id,
|
||||
icon: this.state.icon,
|
||||
colour: this.state.colour,
|
||||
command: this.state.command
|
||||
});
|
||||
this.props.pusherActions.setCommand(this.state);
|
||||
|
||||
window.history.back();
|
||||
|
||||
@ -87769,6 +87765,26 @@ var EditCommand = function (_React$Component) {
|
||||
{ onSubmit: function onSubmit(e) {
|
||||
return _this2.handleSubmit(e);
|
||||
} },
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'field textarea white' },
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'name' },
|
||||
'Name'
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'input' },
|
||||
_react2.default.createElement(_TextField2.default, {
|
||||
name: 'name',
|
||||
value: this.state.name,
|
||||
onChange: function onChange(value) {
|
||||
return _this2.setState({ name: value });
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'field radio white' },
|
||||
@ -87814,33 +87830,84 @@ var EditCommand = function (_React$Component) {
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'name' },
|
||||
'Command'
|
||||
'URL'
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'input' },
|
||||
_react2.default.createElement(_TextField2.default, {
|
||||
name: 'url',
|
||||
value: this.state.url,
|
||||
onChange: function onChange(value) {
|
||||
return _this2.setState({ url: value });
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'field radio white' },
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'name' },
|
||||
'Method'
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'input' },
|
||||
_react2.default.createElement(
|
||||
'label',
|
||||
null,
|
||||
_react2.default.createElement('input', {
|
||||
type: 'radio',
|
||||
name: 'method',
|
||||
value: 'GET',
|
||||
checked: this.state.method == 'GET',
|
||||
onChange: function onChange(e) {
|
||||
return _this2.setState({ method: e.target.value });
|
||||
} }),
|
||||
_react2.default.createElement(
|
||||
'span',
|
||||
{ className: 'label' },
|
||||
'GET'
|
||||
)
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'label',
|
||||
null,
|
||||
_react2.default.createElement('input', {
|
||||
type: 'radio',
|
||||
name: 'method',
|
||||
value: 'POST',
|
||||
checked: this.state.method == 'POST',
|
||||
onChange: function onChange(e) {
|
||||
return _this2.setState({ method: e.target.value });
|
||||
} }),
|
||||
_react2.default.createElement(
|
||||
'span',
|
||||
{ className: 'label' },
|
||||
'POST'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
this.state.method == 'POST' && _react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'field textarea white' },
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'name' },
|
||||
'Data'
|
||||
),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'input' },
|
||||
_react2.default.createElement('textarea', {
|
||||
name: 'command',
|
||||
value: this.state.command,
|
||||
value: this.state.post_data,
|
||||
onChange: function onChange(e) {
|
||||
return _this2.setState({ command: e.target.value });
|
||||
} }),
|
||||
_react2.default.createElement(
|
||||
'div',
|
||||
{ className: 'description' },
|
||||
'Ajax request settings. See ',
|
||||
_react2.default.createElement(
|
||||
'a',
|
||||
{ href: 'http://api.jquery.com/jquery.ajax/', target: '_blank', noopener: 'true' },
|
||||
_react2.default.createElement(
|
||||
'code',
|
||||
null,
|
||||
'jquery.ajax'
|
||||
),
|
||||
' documentation'
|
||||
),
|
||||
'.'
|
||||
)
|
||||
return _this2.setState({ post_data: e.target.value });
|
||||
} })
|
||||
)
|
||||
),
|
||||
_react2.default.createElement(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -98,7 +98,7 @@
|
||||
|
||||
// Release details
|
||||
// These are automatically injected to built HTML
|
||||
var build = "1555118355";
|
||||
var build = "1555274194";
|
||||
var version = "3.35.1";
|
||||
|
||||
// Construct the script tag
|
||||
|
||||
@ -147,7 +147,7 @@ class OutputControl extends React.Component{
|
||||
<div
|
||||
key={command.id}
|
||||
className="commands__item commands__item--interactive"
|
||||
onClick={e => this.props.pusherActions.sendCommand(command.id)}>
|
||||
onClick={e => this.props.pusherActions.runCommand(command.id)}>
|
||||
<Icon className="commands__item__icon" name={command.icon} />
|
||||
<span className={command.colour+'-background commands__item__background'}></span>
|
||||
</div>
|
||||
|
||||
@ -53,6 +53,12 @@ export function startSearch(search_type, query, only_mopidy = false){
|
||||
}
|
||||
|
||||
export function handleException(message, data = {}, description = null, show_notification = true){
|
||||
if (!message && data.message){
|
||||
message = data.message;
|
||||
}
|
||||
if (!description && data.description){
|
||||
description = data.description;
|
||||
}
|
||||
return {
|
||||
type: 'HANDLE_EXCEPTION',
|
||||
message: message,
|
||||
|
||||
@ -223,9 +223,9 @@ export function removeCommand(id){
|
||||
}
|
||||
}
|
||||
|
||||
export function sendCommand(id, notify = false){
|
||||
export function runCommand(id, notify = false){
|
||||
return {
|
||||
type: 'PUSHER_SEND_COMMAND',
|
||||
type: 'PUSHER_RUN_COMMAND',
|
||||
id: id,
|
||||
notify: notify
|
||||
}
|
||||
|
||||
@ -504,38 +504,28 @@ const PusherMiddleware = (function(){
|
||||
next(action);
|
||||
break
|
||||
|
||||
case 'PUSHER_SEND_COMMAND':
|
||||
case 'PUSHER_RUN_COMMAND':
|
||||
var command = Object.assign({}, pusher.commands[action.id]);
|
||||
var notification_key = 'command_'+action.id;
|
||||
|
||||
|
||||
if (action.notify){
|
||||
store.dispatch(uiActions.startProcess(notification_key, 'Sending command'));
|
||||
store.dispatch(uiActions.startProcess(notification_key, 'Running command'));
|
||||
}
|
||||
|
||||
try {
|
||||
var ajax_settings = JSON.parse(command.command);
|
||||
} catch(error){
|
||||
store.dispatch(uiActions.createNotification({key: notification_key, type: 'bad', content: 'Command failed', description: error}));
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle success and failure
|
||||
ajax_settings.success = function(response){
|
||||
console.log("Command sent, response was:",response);
|
||||
|
||||
if (action.notify){
|
||||
|
||||
request(store, 'run_command', {id: action.id})
|
||||
.then(response => {
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
store.dispatch(uiActions.createNotification({key: notification_key, type: 'info', content: 'Command sent'}));
|
||||
}
|
||||
}
|
||||
ajax_settings.error = function(xhr, status, error){
|
||||
console.error("Command failed, response was:",xhr,error);
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
store.dispatch(uiActions.createNotification({key: notification_key, type: 'bad', content: 'Command failed', description: xhr.status+": "+error}));
|
||||
}
|
||||
|
||||
// Actually send the request
|
||||
$.ajax(ajax_settings);
|
||||
if (action.notify){
|
||||
store.dispatch(uiActions.createNotification({key: notification_key, type: 'info', content: 'Command sent'}));
|
||||
}
|
||||
},
|
||||
error => {
|
||||
store.dispatch(uiActions.processFinished(notification_key));
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Could not run command',
|
||||
error
|
||||
));
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
@ -553,7 +543,7 @@ const PusherMiddleware = (function(){
|
||||
radio: response.radio
|
||||
});
|
||||
},
|
||||
error => {
|
||||
error => {
|
||||
store.dispatch(coreActions.handleException(
|
||||
'Could not load radio',
|
||||
error
|
||||
|
||||
@ -169,12 +169,12 @@ class Settings extends React.Component {
|
||||
return (
|
||||
<div className="list__item commands-setup__item list__item--no-interaction" key={command.id}>
|
||||
<div className="col col--w90">
|
||||
<div className="commands__item commands__item--interactive" onClick={e => this.props.pusherActions.sendCommand(command.id, true)}>
|
||||
<div className="commands__item commands__item--interactive" onClick={e => this.props.pusherActions.runCommand(command.id, true)}>
|
||||
<Icon className="commands__item__icon" name={command.icon} />
|
||||
<span className={command.colour+'-background commands__item__background'}></span>
|
||||
</div>
|
||||
<div className="commands-setup__item__url commands__item__url">
|
||||
{command.command && command.command.url ? command.command.url : "-"}
|
||||
{command.name ? command.name : <span className="grey-text">{command.url}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="commands-setup__item__actions">
|
||||
|
||||
@ -9,6 +9,7 @@ import Link from '../../components/Link';
|
||||
import Icon from '../../components/Icon';
|
||||
import ColourField from '../../components/Fields/ColourField';
|
||||
import IconField from '../../components/Fields/IconField';
|
||||
import TextField from '../../components/Fields/TextField';
|
||||
|
||||
import * as pusherActions from '../../services/pusher/actions';
|
||||
import * as uiActions from '../../services/ui/actions';
|
||||
@ -21,8 +22,11 @@ class EditCommand extends React.Component{
|
||||
this.state = {
|
||||
id: helpers.generateGuid(),
|
||||
icon: 'power_settings_new',
|
||||
name: '',
|
||||
colour: '',
|
||||
command: '{"url":"https://'+window.location.hostname+'/broadlink/sendCommand/power/"}'
|
||||
url: "https://"+window.location.hostname+"/broadlink/sendCommand/power/",
|
||||
method: 'GET',
|
||||
post_data: ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,12 +42,7 @@ class EditCommand extends React.Component{
|
||||
handleSubmit(e){
|
||||
e.preventDefault();
|
||||
|
||||
this.props.pusherActions.setCommand({
|
||||
id: this.state.id,
|
||||
icon: this.state.icon,
|
||||
colour: this.state.colour,
|
||||
command: this.state.command
|
||||
});
|
||||
this.props.pusherActions.setCommand(this.state);
|
||||
|
||||
window.history.back();
|
||||
|
||||
@ -121,6 +120,19 @@ class EditCommand extends React.Component{
|
||||
<h1>{this.props.command ? "Edit" : "Create"} command</h1>
|
||||
<form onSubmit={(e) => this.handleSubmit(e)}>
|
||||
|
||||
<div className="field textarea white">
|
||||
<div className="name">
|
||||
Name
|
||||
</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
name="name"
|
||||
value={this.state.name}
|
||||
onChange={value => this.setState({ name: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field radio white">
|
||||
<div className="name">
|
||||
Colour
|
||||
@ -148,19 +160,55 @@ class EditCommand extends React.Component{
|
||||
|
||||
<div className="field textarea white">
|
||||
<div className="name">
|
||||
Command
|
||||
URL
|
||||
</div>
|
||||
<div className="input">
|
||||
<TextField
|
||||
name="url"
|
||||
value={this.state.url}
|
||||
onChange={value => this.setState({ url: value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field radio white">
|
||||
<div className="name">
|
||||
Method
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="method"
|
||||
value="GET"
|
||||
checked={ this.state.method == 'GET' }
|
||||
onChange={ e => this.setState({ method: e.target.value })} />
|
||||
<span className="label">GET</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="method"
|
||||
value="POST"
|
||||
checked={ this.state.method == 'POST' }
|
||||
onChange={ e => this.setState({ method: e.target.value })} />
|
||||
<span className="label">POST</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.method == 'POST' && <div className="field textarea white">
|
||||
<div className="name">
|
||||
Data
|
||||
</div>
|
||||
<div className="input">
|
||||
<textarea
|
||||
name="command"
|
||||
value={this.state.command}
|
||||
onChange={ e => this.setState({ command: e.target.value })}>
|
||||
value={this.state.post_data}
|
||||
onChange={ e => this.setState({ post_data: e.target.value })}>
|
||||
</textarea>
|
||||
<div className="description">
|
||||
Ajax request settings. See <a href="http://api.jquery.com/jquery.ajax/" target="_blank" noopener="true"><code>jquery.ajax</code> documentation</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className="actions centered-text">
|
||||
{this.props.command ? <button type="button" className="button button--destructive button--large" onClick={e => this.handleDelete(e)}>Delete</button> : null}
|
||||
|
||||
Reference in New Issue
Block a user