diff --git a/.github/workflows/codechecks.js.yml b/.github/workflows/codechecks.js.yml
index 6ad2f42e..d1564fc2 100644
--- a/.github/workflows/codechecks.js.yml
+++ b/.github/workflows/codechecks.js.yml
@@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
- node-version: [14.x]
+ node-version: [18.1.0]
steps:
- name: Checkout source
diff --git a/package.json b/package.json
index fdbe017e..2b88642c 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"escodegen": "^2.0.0",
"eslint": "^7.32.0",
"fake-indexeddb": "^3.1.2",
+ "fs-extra": "^11.1.1",
"impunity": "^1.0.9",
"mdn-polyfills": "^5.20.0",
"merge-options": "^3.0.4",
diff --git a/playwright/plugins/dex/index.ts b/playwright/plugins/dex/index.ts
index f45257fc..a5efde10 100644
--- a/playwright/plugins/dex/index.ts
+++ b/playwright/plugins/dex/index.ts
@@ -14,15 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-///
-
import * as path from "path";
import * as os from "os";
import * as fse from "fs-extra";
import {dockerRun, dockerStop } from "../docker";
-// A cypress plugins to add command to start & stop dex instances
+// A plugin that adds command to start & stop dex instances
interface DexConfig {
configDir: string;
diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts
index b0080d6c..75b9d90f 100644
--- a/playwright/plugins/docker/index.ts
+++ b/playwright/plugins/docker/index.ts
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-///
-
import * as os from "os";
import * as childProcess from "child_process";
import * as fse from "fs-extra";
@@ -36,7 +34,8 @@ export function dockerRun(args: {
}
return new Promise((resolve, reject) => {
- childProcess.execFile('docker', [
+ childProcess.execFile('sudo', [
+ "docker",
"run",
"--name", args.containerName,
"-d",
@@ -57,7 +56,8 @@ export function dockerExec(args: {
params: string[];
}): Promise {
return new Promise((resolve, reject) => {
- childProcess.execFile("docker", [
+ childProcess.execFile("sudo", [
+ "docker",
"exec", args.containerId,
...args.params,
], { encoding: 'utf8' }, (err, stdout, stderr) => {
@@ -79,10 +79,12 @@ export function dockerCreateNetwork(args: {
networkName: string;
}): Promise {
return new Promise((resolve, reject) => {
- childProcess.execFile("docker", [
+ childProcess.execFile("sudo", [
+ "docker",
"network",
"create",
- args.networkName
+ args.networkName,
+ "--subnet", "172.18.0.0/16"
], { encoding: 'utf8' }, (err, stdout, stderr) => {
if(err) {
if (stderr.includes(`network with name ${args.networkName} already exists`)) {
@@ -106,7 +108,8 @@ export async function dockerLogs(args: {
const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore";
await new Promise((resolve) => {
- childProcess.spawn("docker", [
+ childProcess.spawn("sudo", [
+ "docker",
"logs",
args.containerId,
], {
@@ -122,7 +125,8 @@ export function dockerStop(args: {
containerId: string;
}): Promise {
return new Promise((resolve, reject) => {
- childProcess.execFile('docker', [
+ childProcess.execFile('sudo', [
+ "docker",
"stop",
args.containerId,
], err => {
@@ -138,7 +142,8 @@ export function dockerRm(args: {
containerId: string;
}): Promise {
return new Promise((resolve, reject) => {
- childProcess.execFile('docker', [
+ childProcess.execFile('sudo', [
+ "docker",
"rm",
args.containerId,
], err => {
diff --git a/playwright/plugins/synapsedocker/index.ts b/playwright/plugins/synapsedocker/index.ts
index 390888cd..ecfc29f0 100644
--- a/playwright/plugins/synapsedocker/index.ts
+++ b/playwright/plugins/synapsedocker/index.ts
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-///
-
import * as path from "path";
import * as os from "os";
import * as crypto from "crypto";
@@ -25,7 +23,7 @@ import {dockerCreateNetwork, dockerExec, dockerLogs, dockerRun, dockerStop} from
import {request} from "@playwright/test";
-// A cypress plugins to add command to start & stop synapses in
+// A plugin to add command to start & stop synapses in
// docker with preset templates.
interface SynapseConfig {
diff --git a/playwright/tests/invite.spec.ts b/playwright/tests/invite.spec.ts
new file mode 100644
index 00000000..90648bf4
--- /dev/null
+++ b/playwright/tests/invite.spec.ts
@@ -0,0 +1,86 @@
+/*
+Copyright 2023 The Matrix.org Foundation C.I.C.
+
+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 {test} from '@playwright/test';
+import {synapseStart, synapseStop, registerUser} from "../plugins/synapsedocker";
+import type {SynapseInstance} from "../plugins/synapsedocker";
+
+test.describe("Login", () => {
+ let synapse: SynapseInstance;
+
+ test.beforeEach(async () => {
+ synapse = await synapseStart("default");
+ });
+
+ test.afterEach(async () => {
+ await synapseStop(synapse.synapseId);
+ });
+
+ test("Invite user using /invite command from composer", async ({ page }) => {
+ const user1 = ["foobaraccount", "password123"] as const;
+ const user2 = ["foobaraccount2", "password123"] as const;
+ await registerUser(synapse, ...user1);
+ const { userId } = await registerUser(synapse, ...user2);
+ await page.goto("/");
+ await page.locator("#homeserver").fill("");
+ await page.locator("#homeserver").type(synapse.baseUrl);
+ const [username, password] = user1;
+ await page.locator("#username").type(username);
+ await page.locator("#password").type(password);
+ await page.getByText("Log In", { exact: true }).click();
+ await page.locator(".SessionView").waitFor();
+ // Create the room
+ await page.getByLabel("Create room").click();
+ await page.getByText("Create Room").click();
+ await page.locator("#name").type("My Room");
+ await page.locator(".CreateRoomView_detailsForm")
+ .getByRole("button", { name: "Create room" })
+ .click();
+ await page.locator(".RoomList")
+ .locator("li")
+ .first()
+ .click();
+ await page.locator(".MessageComposer_input textarea").type(`/invite ${userId}`);
+ await page.keyboard.press("Enter");
+ await page.locator(".AnnouncementView").last().getByText("was invited to the room").waitFor();
+ });
+
+ test("Error is shown when using /invite command from composer", async ({ page }) => {
+ const user1 = ["foobaraccount", "password123"] as const;
+ await registerUser(synapse, ...user1);
+ await page.goto("/");
+ await page.locator("#homeserver").fill("");
+ await page.locator("#homeserver").type(synapse.baseUrl);
+ const [username, password] = user1;
+ await page.locator("#username").type(username);
+ await page.locator("#password").type(password);
+ await page.getByText("Log In", { exact: true }).click();
+ await page.locator(".SessionView").waitFor();
+ // Create the room
+ await page.getByLabel("Create room").click();
+ await page.getByText("Create Room").click();
+ await page.locator("#name").type("My Room");
+ await page.locator(".CreateRoomView_detailsForm")
+ .getByRole("button", { name: "Create room" })
+ .click();
+ await page.locator(".RoomList")
+ .locator("li")
+ .first()
+ .click();
+ await page.locator(".MessageComposer_input textarea").type("/invite foobar");
+ await page.keyboard.press("Enter");
+ await page.locator(".RoomView").locator(".ErrorView").waitFor();
+ });
+});
diff --git a/scripts/test-app.sh b/scripts/test-app.sh
index cfbd37a3..548ea6c4 100755
--- a/scripts/test-app.sh
+++ b/scripts/test-app.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# Make sure docker is available
-if ! docker info > /dev/null 2>&1; then
+if ! docker --version > /dev/null 2>&1; then
echo "You need to intall docker before you can run the tests!"
exit 1
fi
diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js
index 81835fb0..1e2c9af6 100644
--- a/src/domain/session/room/RoomViewModel.js
+++ b/src/domain/session/room/RoomViewModel.js
@@ -273,6 +273,14 @@ export class RoomViewModel extends ErrorReportViewModel {
this.reportError(new Error("join syntax: /join "));
}
break;
+ case "invite":
+ if (args.length === 1) {
+ const userId = args[0];
+ await this._room.inviteUser(userId);
+ } else {
+ this.reportError(new Error("invite syntax: /invite "));
+ }
+ break;
case "shrug":
message = "¯\\_(ツ)_/¯ " + args.join(" ");
msgtype = "m.text";
diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts
index c5f90555..eebc692a 100644
--- a/src/matrix/net/HomeServerApi.ts
+++ b/src/matrix/net/HomeServerApi.ts
@@ -267,6 +267,15 @@ export class HomeServerApi {
return this._get("/pushers", undefined, undefined, options);
}
+ invite(roomId: string, userId: string, reason?: string, options?: BaseRequestOptions): IHomeServerRequest {
+ return this._post(
+ `/rooms/${encodeURIComponent(roomId)}/invite`,
+ {},
+ { user_id: userId, reason },
+ options
+ );
+ }
+
join(roomId: string, options?: BaseRequestOptions): IHomeServerRequest {
return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options);
}
diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js
index 47da3c03..73074f7a 100644
--- a/src/matrix/room/Room.js
+++ b/src/matrix/room/Room.js
@@ -470,6 +470,13 @@ export class Room extends BaseRoom {
});
}
+ async inviteUser(userId, reason) {
+ if (!userId) {
+ throw new Error("userId is null/undefined");
+ }
+ await this._hsApi.invite(this.id, userId, reason).response();
+ }
+
/* called by BaseRoom to pass pendingEvents when opening the timeline */
_getPendingEvents() {
return this._sendQueue.pendingEvents;
diff --git a/yarn.lock b/yarn.lock
index f7540f2d..268b6e6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -83,12 +83,12 @@
fastq "^1.6.0"
"@playwright/test@^1.27.1":
- version "1.32.1"
- resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.1.tgz#749c9791adb048c266277a39ba0f7e33fe593ffe"
- integrity sha512-FTwjCuhlm1qHUGf4hWjfr64UMJD/z0hXYbk+O387Ioe6WdyZQ+0TBDAc6P+pHjx2xCv1VYNgrKbYrNixFWy4Dg==
+ version "1.37.1"
+ resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.37.1.tgz#e7f44ae0faf1be52d6360c6bbf689fd0057d9b6f"
+ integrity sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg==
dependencies:
"@types/node" "*"
- playwright-core "1.32.1"
+ playwright-core "1.37.1"
optionalDependencies:
fsevents "2.3.2"
@@ -1000,6 +1000,15 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469"
integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+fs-extra@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
+ integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1065,6 +1074,11 @@ globby@^11.0.3:
merge2 "^1.3.0"
slash "^3.0.0"
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -1199,6 +1213,15 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+jsonfile@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+ integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+ dependencies:
+ universalify "^2.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
@@ -1384,10 +1407,10 @@ picomatch@^2.2.3:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
-playwright-core@1.32.1:
- version "1.32.1"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.1.tgz#5a10c32403323b07d75ea428ebeed866a80b76a1"
- integrity sha512-KZYUQC10mXD2Am1rGlidaalNGYk3LU1vZqqNk0gT4XPty1jOqgup8KDP8l2CUlqoNKhXM5IfGjWgW37xvGllBA==
+playwright-core@1.37.1:
+ version "1.37.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126"
+ integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA==
postcss-css-variables@^0.18.0:
version "0.18.0"
@@ -1714,6 +1737,11 @@ typeson@^6.0.0:
resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b"
integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==
+universalify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+ integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"