diff --git a/package.json b/package.json index 8035fbb508..f3c47ac491 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,11 @@ "author": "", "license": "ISC", "dependencies": { + "cheerio": "^1.0.0-rc.2", "commander": "^2.17.1", - "puppeteer": "^1.6.0" + "puppeteer": "^1.6.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "uuid": "^3.3.2" } } diff --git a/src/rest/consent.js b/src/rest/consent.js new file mode 100644 index 0000000000..1e36f541a3 --- /dev/null +++ b/src/rest/consent.js @@ -0,0 +1,30 @@ +/* +Copyright 2018 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. +*/ + +const request = require('request-promise-native'); +const cheerio = require('cheerio'); +const url = require("url"); + +module.exports.approveConsent = async function(consentUrl) { + const body = await request.get(consentUrl); + const doc = cheerio.load(body); + const v = doc("input[name=v]").val(); + const u = doc("input[name=u]").val(); + const h = doc("input[name=h]").val(); + const formAction = doc("form").attr("action"); + const absAction = url.resolve(consentUrl, formAction); + await request.post(absAction).form({v, u, h}); +}; diff --git a/src/rest/factory.js b/src/rest/factory.js new file mode 100644 index 0000000000..2df6624ef9 --- /dev/null +++ b/src/rest/factory.js @@ -0,0 +1,77 @@ +/* +Copyright 2018 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. +*/ + +const util = require('util'); +const exec = util.promisify(require('child_process').exec); +const request = require('request-promise-native'); +const RestSession = require('./session'); + +module.exports = class RestSessionFactory { + constructor(synapseSubdir, hsUrl, cwd) { + this.synapseSubdir = synapseSubdir; + this.hsUrl = hsUrl; + this.cwd = cwd; + } + + async createSession(username, password) { + await this._register(username, password); + const authResult = await this._authenticate(username, password); + return new RestSession(authResult); + } + + _register(username, password) { + const registerArgs = [ + '-c homeserver.yaml', + `-u ${username}`, + `-p ${password}`, + // '--regular-user', + '-a', //until PR gets merged + this.hsUrl + ]; + const registerCmd = `./scripts/register_new_matrix_user ${registerArgs.join(' ')}`; + const allCmds = [ + `cd ${this.synapseSubdir}`, + "source env/bin/activate", + registerCmd + ].join(';'); + + return exec(allCmds, {cwd: this.cwd, encoding: 'utf-8'}).catch((result) => { + const lines = result.stdout.trim().split('\n'); + const failureReason = lines[lines.length - 1]; + throw new Error(`creating user ${username} failed: ${failureReason}`); + }); + } + + async _authenticate(username, password) { + const requestBody = { + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": username + }, + "password": password + }; + const url = `${this.hsUrl}/_matrix/client/r0/login`; + const responseBody = await request.post({url, json: true, body: requestBody}); + return { + accessToken: responseBody.access_token, + homeServer: responseBody.home_server, + userId: responseBody.user_id, + deviceId: responseBody.device_id, + hsUrl: this.hsUrl, + }; + } +} diff --git a/src/rest/room.js b/src/rest/room.js new file mode 100644 index 0000000000..b7da1789ff --- /dev/null +++ b/src/rest/room.js @@ -0,0 +1,42 @@ +/* +Copyright 2018 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. +*/ + +const uuidv4 = require('uuid/v4'); + +/* no pun intented */ +module.exports = class RestRoom { + constructor(session, roomId) { + this.session = session; + this.roomId = roomId; + } + + async talk(message) { + const txId = uuidv4(); + await this.session._put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, { + "msgtype": "m.text", + "body": message + }); + return txId; + } + + async leave() { + await this.session._post(`/rooms/${this.roomId}/leave`); + } + + id() { + return this.roomId; + } +} diff --git a/src/rest/session.js b/src/rest/session.js new file mode 100644 index 0000000000..f57d0467f5 --- /dev/null +++ b/src/rest/session.js @@ -0,0 +1,88 @@ +/* +Copyright 2018 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. +*/ + +const request = require('request-promise-native'); +const RestRoom = require('./room'); +const {approveConsent} = require('./consent'); + +module.exports = class RestSession { + constructor(credentials) { + this.credentials = credentials; + } + + _post(csApiPath, body) { + return this._request("POST", csApiPath, body); + } + + _put(csApiPath, body) { + return this._request("PUT", csApiPath, body); + } + + async _request(method, csApiPath, body) { + try { + const responseBody = await request({ + url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`, + method, + headers: { + "Authorization": `Bearer ${this.credentials.accessToken}` + }, + json: true, + body + }); + return responseBody; + + } catch(err) { + const responseBody = err.response.body; + if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') { + await approveConsent(responseBody.consent_uri); + return this._request(method, csApiPath, body); + } else if(responseBody && responseBody.error) { + throw new Error(`${method} ${csApiPath}: ${responseBody.error}`); + } else { + throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`); + } + } + } + + async join(roomId) { + const {room_id} = await this._post(`/rooms/${roomId}/join`); + return new RestRoom(this, room_id); + } + + + async createRoom(name, options) { + const body = { + name, + }; + if (options.invite) { + body.invite = options.invite; + } + if (options.public) { + body.visibility = "public"; + } else { + body.visibility = "private"; + } + if (options.dm) { + body.is_direct = true; + } + if (options.topic) { + body.topic = options.topic; + } + + const {room_id} = await this._post(`/createRoom`, body); + return new RestRoom(this, room_id); + } +}