-
-
Notifications
You must be signed in to change notification settings - Fork 827
Bi-directional widget postMessaging API (stickerpacks) [WIP] #1672
Changes from 145 commits
26c6c25
3a89b90
c9b8aab
f410112
c234e20
56f497d
9f733eb
f2ad7be
954c6ee
e63f569
83f9a41
774774c
8e5c3f0
536d4ef
08bcfc5
baf472b
3724a1a
7b59774
7660176
a408b98
e96d199
b85efa0
eb4053b
32aecd0
da199da
d20aebf
54671ab
1c8586e
d652f11
7b313b7
d256e47
5724749
54d1286
dc14230
486b2cf
5df9a01
0577316
adebf71
60e7646
9e9de76
6b0b25c
9abb160
53b590f
2bb51ba
52f28d0
1a994b8
2cf9da8
78bd25e
d0c16fa
4f36709
1ab71f6
9df4dae
90b7cb3
2354361
a3c6dd3
351bbdf
0fab905
5e6da4d
5a9a4ea
82b9897
87d8ed5
cb7f25f
86542d8
9339284
7676fc0
38ed01b
86da204
614a10c
3331c8a
5a42712
fa336b7
aa524c3
b6f85fb
f8d7ab1
1293c53
917d85d
910623d
23bef68
0441487
29962ed
34de372
992c477
e508f06
393236b
f3943be
7b75dbb
5e30468
234ca8b
9e3c1fb
5559341
ce560c5
9a5c916
88288ff
fefc325
46f94b3
57b027b
d755b82
9b667f2
9ae89e2
b2bf4d4
891199b
332892f
86461bc
ee4310c
e249e3d
c93faf7
2b0790b
20a442c
d3de44e
14d52c9
707e3f3
73c8ef5
b64736a
5ca0fc3
8e7564b
ef4d137
57c98d9
0fdbddf
21c8bed
f3c928a
e2cedbe
7755a3c
d5465cf
b529edb
7f91b47
b2bb15b
c59dd5b
3ab8b1f
fdec4b3
e36ae3c
e7c19fd
a338593
7e06209
46f46ee
b2d23b6
a81269c
de33294
53b716b
f820374
7d13edc
8b311c7
66ea78d
83412ac
4d8f507
7462812
4ac9653
b4e70e3
5fc9b8a
a1581ad
23a52bd
35fcb2c
aefccb1
cafbd29
f383298
9c10d24
38c8bc7
e21cc14
67f755e
c8f9586
6181ca6
b1e7dcf
3d9bdb9
5ba18b5
20cbc01
9a3f356
93804e8
2e6d6c8
003cf61
ff0834a
9cc3d3c
557a45e
11915b0
d83b6f1
49bea1a
8241afe
b109c93
f8f8bc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -275,6 +275,12 @@ class ContentMessages { | |
this.nextId = 0; | ||
} | ||
|
||
sendStickerContentToRoom(url, roomId, info, text, matrixClient) { | ||
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { | ||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); | ||
}); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4 space indents please :) |
||
sendContentToRoom(file, roomId, matrixClient) { | ||
const content = { | ||
body: file.name || 'Attachment', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
Copyright 2017 New Vector Ltd | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
import Modal from './Modal'; | ||
import sdk from './index'; | ||
import SdkConfig from './SdkConfig'; | ||
import ScalarMessaging from './ScalarMessaging'; | ||
import ScalarAuthClient from './ScalarAuthClient'; | ||
import RoomViewStore from './stores/RoomViewStore'; | ||
|
||
if (!global.mxIntegrationManager) { | ||
global.mxIntegrationManager = {}; | ||
} | ||
|
||
export default class IntegrationManager { | ||
static async _init() { | ||
if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) { | ||
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { | ||
ScalarMessaging.startListening(); | ||
global.mxIntegrationManager.client = new ScalarAuthClient(); | ||
|
||
await global.mxIntegrationManager.client.connect().then(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. more mixing of async/await and promises style: this looks fine logic-wise but it makes it harder to read |
||
global.mxIntegrationManager.connected = true; | ||
}).catch((e) => { | ||
console.error("Failed to connect to integrations server", e); | ||
global.mxIntegrationManager.error = e; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could end up with multiple in the constructor:
in other functions:
You could also make things simpler here by doing:
...rather than exporting the class itself, then you don't need to make the member functions static and you can just use variables on |
||
}); | ||
} else { | ||
console.error('Invalid integration manager config', SdkConfig.get()); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Launch the integrations manager on the stickers integration page | ||
* @param {string} integType integration / widget type | ||
* @param {string} integId integration / widget ID | ||
* @param {function} onClose Callback to invoke on integration manager close | ||
*/ | ||
static async open(integType, integId, onClose) { | ||
await IntegrationManager._init(); | ||
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); | ||
if (global.mxIntegrationManager.error || | ||
!(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) { | ||
console.error("Scalar error", global.mxIntegrationManager); | ||
return; | ||
} | ||
integType = 'type_' + integType; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mmm, redefining function params can be confusing |
||
const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ? | ||
global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You've already done the |
||
RoomViewStore.getRoomId(), | ||
integType, | ||
integId, | ||
) : | ||
null; | ||
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { | ||
src: src, | ||
}, "mx_IntegrationsManager"); | ||
|
||
if (onClose) { | ||
onClose(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it'll be invoked just as the dialog opens rather than when it closes? |
||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
Copyright 2017 New Vector Ltd | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
import Promise from "bluebird"; | ||
|
||
// NOTE: PostMessageApi only handles message events with a data payload with a | ||
// response field | ||
export default class PostMessageApi { | ||
constructor(targetWindow, timeoutMs) { | ||
this._window = targetWindow || window.parent; // default to parent window | ||
this._timeoutMs = timeoutMs || 5000; // default to 5s timer | ||
this._counter = 0; | ||
this._requestMap = { | ||
// $ID: {resolve, reject} | ||
}; | ||
} | ||
|
||
start() { | ||
addEventListener('message', this.getOnMessageCallback()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think since |
||
} | ||
|
||
stop() { | ||
removeEventListener('message', this.getOnMessageCallback()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also might return something other than the thing we passed to addEventListener edit: oh wait, no it doesn't |
||
} | ||
|
||
// Somewhat convoluted so we can successfully capture the PostMessageApi 'this' instance. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep, I think all that's required here is a bound function? |
||
getOnMessageCallback() { | ||
if (this._onMsgCallback) { | ||
return this._onMsgCallback; | ||
} | ||
const self = this; | ||
this._onMsgCallback = function(ev) { | ||
// THIS IS ALL UNSAFE EXECUTION. | ||
// We do not verify who the sender of `ev` is! | ||
const payload = ev.data; | ||
// NOTE: Workaround for running in a mobile WebView where a | ||
// postMessage immediately triggers this callback even though it is | ||
// not the response. | ||
if (payload.response === undefined) { | ||
return; | ||
} | ||
const promise = self._requestMap[payload._id]; | ||
if (!promise) { | ||
return; | ||
} | ||
delete self._requestMap[payload._id]; | ||
promise.resolve(payload); | ||
}; | ||
return this._onMsgCallback; | ||
} | ||
|
||
exec(action, target) { | ||
this._counter += 1; | ||
target = target || "*"; | ||
action._id = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter; | ||
|
||
return new Promise((resolve, reject) => { | ||
this._requestMap[action._id] = {resolve, reject}; | ||
this._window.postMessage(action, target); | ||
|
||
if (this._timeoutMs > 0) { | ||
setTimeout(() => { | ||
if (!this._requestMap[action._id]) { | ||
return; | ||
} | ||
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action)); | ||
this._requestMap[action._id].reject(new Error("Timed out")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. presumably the request needs to be deleted from the map here too? |
||
}, this._timeoutMs); | ||
} | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,10 +148,48 @@ class ScalarAuthClient { | |
return defer.promise; | ||
} | ||
|
||
getScalarInterfaceUrlForRoom(roomId, screen, id) { | ||
/** | ||
* Mark all assets associated with the specified widget as "disabled" in the | ||
* integration manager database. | ||
* This can be useful to temporarily prevent purchased assets from being displayed. | ||
* @param {string} widgetType [description] | ||
* @param {string} widgetId [description] | ||
* @return {Promise} Resolves on completion | ||
*/ | ||
disableWidgetAssets(widgetType, widgetId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs on this would be nice - mainly for those implementing the API call. |
||
let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state'; | ||
url = this.getStarterLink(url); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this function probably wants renaming? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed IRL, I'm going to leave this. |
||
return new Promise((resolve, reject) => { | ||
request({ | ||
method: 'GET', | ||
uri: url, | ||
json: true, | ||
qs: { | ||
'widget_type': widgetType, | ||
'widget_id': widgetId, | ||
'state': 'disable', | ||
}, | ||
}, (err, response, body) => { | ||
if (err) { | ||
reject(err); | ||
} else if (response.statusCode / 100 !== 2) { | ||
reject({statusCode: response.statusCode}); | ||
} else if (!body) { | ||
reject(new Error("Failed to set widget assets state")); | ||
} else { | ||
resolve(); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
getScalarInterfaceUrlForRoom(room, screen, id) { | ||
const roomId = room.roomId; | ||
const roomName = room.name; | ||
let url = SdkConfig.get().integrations_ui_url; | ||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken); | ||
url += "&room_id=" + encodeURIComponent(roomId); | ||
url += "&room_name=" + encodeURIComponent(roomName); | ||
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); | ||
if (id) { | ||
url += '&integ_id=' + encodeURIComponent(id); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -235,6 +235,7 @@ const SdkConfig = require('./SdkConfig'); | |
const MatrixClientPeg = require("./MatrixClientPeg"); | ||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent; | ||
const dis = require("./dispatcher"); | ||
const Widgets = require('./utils/widgets'); | ||
import { _t } from './languageHandler'; | ||
|
||
function sendResponse(event, res) { | ||
|
@@ -291,6 +292,7 @@ function setWidget(event, roomId) { | |
const widgetUrl = event.data.url; | ||
const widgetName = event.data.name; // optional | ||
const widgetData = event.data.data; // optional | ||
const userWidget = event.data.userWidget; | ||
|
||
const client = MatrixClientPeg.get(); | ||
if (!client) { | ||
|
@@ -330,17 +332,51 @@ function setWidget(event, roomId) { | |
name: widgetName, | ||
data: widgetData, | ||
}; | ||
if (widgetUrl === null) { // widget is being deleted | ||
content = {}; | ||
} | ||
|
||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => { | ||
if (userWidget) { | ||
const client = MatrixClientPeg.get(); | ||
const userWidgets = Widgets.getUserWidgets(); | ||
|
||
// Delete existing widget with ID | ||
try { | ||
delete userWidgets[widgetId]; | ||
} catch (e) { | ||
console.error(`$widgetId is non-configurable`); | ||
} | ||
|
||
// Add new widget / update | ||
if (widgetUrl !== null) { | ||
userWidgets[widgetId] = { | ||
content: content, | ||
sender: client.getUserId(), | ||
stateKey: widgetId, | ||
type: 'im.vector.modular.widgets', | ||
id: widgetId, | ||
}; | ||
} | ||
|
||
client.setAccountData('m.widgets', userWidgets); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we wait for this to complete before sending the response? |
||
sendResponse(event, { | ||
success: true, | ||
}); | ||
}, (err) => { | ||
sendError(event, _t('Failed to send request.'), err); | ||
}); | ||
|
||
dis.dispatch({ action: "user_widget_updated" }); | ||
} else { // Room widget | ||
if (!roomId) { | ||
sendError(event, _t('Missing roomId.'), null); | ||
} | ||
|
||
if (widgetUrl === null) { // widget is being deleted | ||
content = {}; | ||
} | ||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per IRL, should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned below, no, we're leaving the event type for room widgets as is for the time being. I've added a TODO comment to indicate that this needs updating / fixing in due course. |
||
sendResponse(event, { | ||
success: true, | ||
}); | ||
}, (err) => { | ||
sendError(event, _t('Failed to send request.'), err); | ||
}); | ||
} | ||
} | ||
|
||
function getWidgets(event, roomId) { | ||
|
@@ -349,19 +385,28 @@ function getWidgets(event, roomId) { | |
sendError(event, _t('You need to be logged in.')); | ||
return; | ||
} | ||
const room = client.getRoom(roomId); | ||
if (!room) { | ||
sendError(event, _t('This room is not recognised.')); | ||
return; | ||
} | ||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); | ||
// Only return widgets which have required fields | ||
const widgetStateEvents = []; | ||
stateEvents.forEach((ev) => { | ||
if (ev.getContent().type && ev.getContent().url) { | ||
widgetStateEvents.push(ev.event); // return the raw event | ||
let widgetStateEvents = []; | ||
|
||
if (roomId) { | ||
const room = client.getRoom(roomId); | ||
if (!room) { | ||
sendError(event, _t('This room is not recognised.')); | ||
return; | ||
} | ||
}); | ||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets"); | ||
// Only return widgets which have required fields | ||
if (room) { | ||
stateEvents.forEach((ev) => { | ||
if (ev.getContent().type && ev.getContent().url) { | ||
widgetStateEvents.push(ev.event); // return the raw event | ||
} | ||
}); | ||
} | ||
} | ||
|
||
// Add user widgets (not linked to a specific room) | ||
const userWidgets = Widgets.getUserWidgetsArray(); | ||
widgetStateEvents = widgetStateEvents.concat(userWidgets); | ||
|
||
sendResponse(event, widgetStateEvents); | ||
} | ||
|
@@ -578,9 +623,22 @@ const onMessage = function(event) { | |
|
||
const roomId = event.data.room_id; | ||
const userId = event.data.user_id; | ||
|
||
if (!roomId) { | ||
sendError(event, _t('Missing room_id in request')); | ||
return; | ||
// These APIs don't require roomId | ||
// Get and set user widgets (not associated with a specific room) | ||
// If roomId is specified, it must be validated, so room-based widgets agreed | ||
// handled further down. | ||
if (event.data.action === "get_widgets") { | ||
getWidgets(event, null); | ||
return; | ||
} else if (event.data.action === "set_widget") { | ||
setWidget(event, null); | ||
return; | ||
} else { | ||
sendError(event, _t('Missing room_id in request')); | ||
return; | ||
} | ||
} | ||
let promise = Promise.resolve(currentRoomId); | ||
if (!currentRoomId) { | ||
|
@@ -601,6 +659,15 @@ const onMessage = function(event) { | |
return; | ||
} | ||
|
||
// Get and set room-based widgets | ||
if (event.data.action === "get_widgets") { | ||
getWidgets(event, roomId); | ||
return; | ||
} else if (event.data.action === "set_widget") { | ||
setWidget(event, roomId); | ||
return; | ||
} | ||
|
||
// These APIs don't require userId | ||
if (event.data.action === "join_rules_state") { | ||
getJoinRules(event, roomId); | ||
|
@@ -611,12 +678,6 @@ const onMessage = function(event) { | |
} else if (event.data.action === "get_membership_count") { | ||
getMembershipCount(event, roomId); | ||
return; | ||
} else if (event.data.action === "set_widget") { | ||
setWidget(event, roomId); | ||
return; | ||
} else if (event.data.action === "get_widgets") { | ||
getWidgets(event, roomId); | ||
return; | ||
} else if (event.data.action === "get_room_enc_state") { | ||
getRoomEncState(event, roomId); | ||
return; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this will swallow the error so the error handler you've defined in
injectSticker
won't be called (the promise will succeed). You just need tothrow e
at the end.