2022-06-09 19:44:57 -06:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const assert = require('assert');
|
|
|
|
const urlJoin = require('url-join');
|
2022-11-02 03:27:30 -06:00
|
|
|
const { fetchEndpointAsJson, fetchEndpoint } = require('../../server/lib/fetch-endpoint');
|
2023-04-19 00:26:15 -06:00
|
|
|
const getServerNameFromMatrixRoomIdOrAlias = require('../../server/lib/matrix-utils/get-server-name-from-matrix-room-id-or-alias');
|
2022-06-09 19:44:57 -06:00
|
|
|
|
2022-11-02 03:27:30 -06:00
|
|
|
const config = require('../../server/lib/config');
|
2022-06-09 19:44:57 -06:00
|
|
|
const matrixAccessToken = config.get('matrixAccessToken');
|
|
|
|
assert(matrixAccessToken);
|
2022-08-29 17:56:31 -06:00
|
|
|
const testMatrixServerUrl1 = config.get('testMatrixServerUrl1');
|
|
|
|
assert(testMatrixServerUrl1);
|
2022-06-09 19:44:57 -06:00
|
|
|
|
|
|
|
let txnCount = 0;
|
|
|
|
function getTxnId() {
|
|
|
|
txnCount++;
|
|
|
|
return `${new Date().getTime()}--${txnCount}`;
|
|
|
|
}
|
|
|
|
|
2022-09-15 19:41:55 -06:00
|
|
|
// Basic slugify function, plenty of edge cases and should not be used for
|
|
|
|
// production.
|
|
|
|
function slugify(inputText) {
|
|
|
|
return (
|
|
|
|
inputText
|
|
|
|
.toLowerCase()
|
|
|
|
// Replace whitespace with hyphens
|
|
|
|
.replace(/\s+/g, '-')
|
|
|
|
// Remove anything not alpha-numeric or hypen
|
|
|
|
.replace(/[^a-z0-9-]+/g, '')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-29 17:56:31 -06:00
|
|
|
async function ensureUserRegistered({ matrixServerUrl, username }) {
|
2022-11-03 03:12:00 -06:00
|
|
|
const { data: registerResponse } = await fetchEndpointAsJson(
|
2022-08-29 17:56:31 -06:00
|
|
|
urlJoin(matrixServerUrl, '/_matrix/client/v3/register'),
|
|
|
|
{
|
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
type: 'm.login.dummy',
|
|
|
|
username,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const userId = registerResponse['user_id'];
|
|
|
|
assert(userId);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getTestClientForAs() {
|
|
|
|
return {
|
|
|
|
homeserverUrl: testMatrixServerUrl1,
|
|
|
|
accessToken: matrixAccessToken,
|
|
|
|
userId: '@archiver:hs1',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-06-09 19:44:57 -06:00
|
|
|
// Get client to act with for all of the client methods. This will use the
|
|
|
|
// application service access token and client methods will append `?user_id`
|
|
|
|
// for the specific user to act upon so we can use the `?ts` message timestamp
|
|
|
|
// massaging when sending.
|
|
|
|
async function getTestClientForHs(testMatrixServerUrl) {
|
|
|
|
// Register the virtual user
|
|
|
|
const username = `user-t${new Date().getTime()}-r${Math.floor(Math.random() * 1000000000)}`;
|
2022-11-03 03:12:00 -06:00
|
|
|
const { data: registerResponse } = await fetchEndpointAsJson(
|
2022-06-09 19:44:57 -06:00
|
|
|
urlJoin(testMatrixServerUrl, '/_matrix/client/v3/register'),
|
|
|
|
{
|
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
type: 'm.login.application_service',
|
|
|
|
username,
|
|
|
|
},
|
|
|
|
accessToken: matrixAccessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-06-29 05:56:13 -06:00
|
|
|
const applicationServiceUserIdOverride = registerResponse['user_id'];
|
|
|
|
assert(applicationServiceUserIdOverride);
|
2022-06-09 19:44:57 -06:00
|
|
|
|
|
|
|
return {
|
|
|
|
homeserverUrl: testMatrixServerUrl,
|
|
|
|
// We use the application service AS token because we need to be able to use
|
|
|
|
// the `?ts` timestamp massaging when sending events
|
|
|
|
accessToken: matrixAccessToken,
|
2022-06-29 05:56:13 -06:00
|
|
|
userId: applicationServiceUserIdOverride,
|
|
|
|
applicationServiceUserIdOverride,
|
2022-06-09 19:44:57 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-04-19 00:26:15 -06:00
|
|
|
async function sendEvent({ client, roomId, eventType, stateKey, content, timestamp }) {
|
|
|
|
assert(client);
|
|
|
|
assert(roomId);
|
|
|
|
assert(content);
|
|
|
|
|
|
|
|
let qs = new URLSearchParams();
|
|
|
|
if (timestamp) {
|
|
|
|
assert(
|
|
|
|
timestamp && client.applicationServiceUserIdOverride,
|
|
|
|
'We can only do `?ts` massaging from an application service access token. ' +
|
|
|
|
'Expected `client.applicationServiceUserIdOverride` to be defined so we can act on behalf of that user'
|
|
|
|
);
|
|
|
|
|
|
|
|
qs.append('ts', timestamp);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (client.applicationServiceUserIdOverride) {
|
|
|
|
qs.append('user_id', client.applicationServiceUserIdOverride);
|
|
|
|
}
|
|
|
|
|
|
|
|
let url;
|
|
|
|
if (typeof stateKey === 'string') {
|
|
|
|
url = urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`/_matrix/client/v3/rooms/${encodeURIComponent(
|
|
|
|
roomId
|
|
|
|
)}/state/${eventType}/${stateKey}?${qs.toString()}`
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
url = urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`/_matrix/client/v3/rooms/${encodeURIComponent(
|
|
|
|
roomId
|
|
|
|
)}/send/${eventType}/${getTxnId()}?${qs.toString()}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { data: sendResponse } = await fetchEndpointAsJson(url, {
|
|
|
|
method: 'PUT',
|
|
|
|
body: content,
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
});
|
|
|
|
|
|
|
|
const eventId = sendResponse['event_id'];
|
|
|
|
assert(eventId);
|
|
|
|
return eventId;
|
|
|
|
}
|
|
|
|
|
|
|
|
const WORLD_READABLE_STATE_EVENT = {
|
|
|
|
type: 'm.room.history_visibility',
|
|
|
|
state_key: '',
|
|
|
|
content: {
|
|
|
|
history_visibility: 'world_readable',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2022-06-09 19:44:57 -06:00
|
|
|
// Create a public room to test in
|
2022-09-15 19:41:55 -06:00
|
|
|
async function createTestRoom(client, overrideCreateOptions = {}) {
|
2022-06-09 19:44:57 -06:00
|
|
|
let qs = new URLSearchParams();
|
2022-06-29 05:56:13 -06:00
|
|
|
if (client.applicationServiceUserIdOverride) {
|
|
|
|
qs.append('user_id', client.applicationServiceUserIdOverride);
|
2022-06-09 19:44:57 -06:00
|
|
|
}
|
|
|
|
|
2022-09-15 19:41:55 -06:00
|
|
|
const roomName = overrideCreateOptions.name || 'the hangout spot';
|
|
|
|
const roomAlias = slugify(roomName + getTxnId());
|
|
|
|
|
2022-11-03 03:12:00 -06:00
|
|
|
const { data: createRoomResponse } = await fetchEndpointAsJson(
|
2022-06-09 19:44:57 -06:00
|
|
|
urlJoin(client.homeserverUrl, `/_matrix/client/v3/createRoom?${qs.toString()}`),
|
|
|
|
{
|
|
|
|
method: 'POST',
|
|
|
|
body: {
|
|
|
|
preset: 'public_chat',
|
2022-09-15 19:41:55 -06:00
|
|
|
name: roomName,
|
|
|
|
room_alias_name: roomAlias,
|
2023-04-19 00:26:15 -06:00
|
|
|
initial_state: [WORLD_READABLE_STATE_EVENT],
|
2022-09-15 19:41:55 -06:00
|
|
|
visibility: 'public',
|
2022-09-08 18:15:07 -06:00
|
|
|
...overrideCreateOptions,
|
2022-06-09 19:44:57 -06:00
|
|
|
},
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const roomId = createRoomResponse['room_id'];
|
|
|
|
assert(roomId);
|
|
|
|
return roomId;
|
|
|
|
}
|
|
|
|
|
2023-04-19 00:26:15 -06:00
|
|
|
async function upgradeTestRoom({
|
|
|
|
client,
|
|
|
|
oldRoomId,
|
|
|
|
useMsc3946DynamicPredecessor = false,
|
|
|
|
overrideCreateOptions = {},
|
|
|
|
timestamp,
|
|
|
|
}) {
|
|
|
|
assert(client);
|
|
|
|
assert(oldRoomId);
|
|
|
|
|
|
|
|
const createOptions = {
|
|
|
|
...overrideCreateOptions,
|
|
|
|
};
|
|
|
|
// Setup the pointer from the new room to the old room
|
|
|
|
if (useMsc3946DynamicPredecessor) {
|
|
|
|
createOptions.initial_state = [
|
|
|
|
WORLD_READABLE_STATE_EVENT,
|
|
|
|
{
|
|
|
|
type: 'org.matrix.msc3946.room_predecessor',
|
|
|
|
state_key: '',
|
|
|
|
content: {
|
|
|
|
predecessor_room_id: oldRoomId,
|
|
|
|
via_servers: [getServerNameFromMatrixRoomIdOrAlias(oldRoomId)],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
createOptions.creation_content = {
|
|
|
|
predecessor: {
|
|
|
|
room_id: oldRoomId,
|
|
|
|
// The event ID of the last known event in the old room (supposedly required).
|
|
|
|
//event_id: TODO,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Pass `timestamp` massaging option to `createTestRoom()` when it supports it,
|
|
|
|
// see https://github.com/matrix-org/matrix-public-archive/issues/169
|
|
|
|
const newRoomid = await createTestRoom(client, createOptions);
|
|
|
|
|
|
|
|
// Now send the tombstone event pointing from the old room to the new room
|
|
|
|
const tombstoneEventId = await sendEvent({
|
|
|
|
client,
|
|
|
|
roomId: oldRoomId,
|
|
|
|
eventType: 'm.room.tombstone',
|
|
|
|
stateKey: '',
|
|
|
|
content: {
|
|
|
|
replacement_room: newRoomid,
|
|
|
|
},
|
|
|
|
timestamp,
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
newRoomid,
|
|
|
|
tombstoneEventId,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-27 00:09:13 -06:00
|
|
|
async function getCanonicalAlias({ client, roomId }) {
|
2022-11-03 03:12:00 -06:00
|
|
|
const { data: stateCanonicalAliasRes } = await fetchEndpointAsJson(
|
2022-10-27 00:09:13 -06:00
|
|
|
urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.canonical_alias`
|
|
|
|
),
|
|
|
|
{
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const canonicalAlias = stateCanonicalAliasRes.alias;
|
|
|
|
assert(canonicalAlias, `getCanonicalAlias() did not return canonicalAlias as expected`);
|
|
|
|
|
|
|
|
return canonicalAlias;
|
|
|
|
}
|
|
|
|
|
2022-06-09 19:44:57 -06:00
|
|
|
async function joinRoom({ client, roomId, viaServers }) {
|
|
|
|
let qs = new URLSearchParams();
|
|
|
|
if (viaServers) {
|
|
|
|
[].concat(viaServers).forEach((viaServer) => {
|
|
|
|
qs.append('server_name', viaServer);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-06-29 05:56:13 -06:00
|
|
|
if (client.applicationServiceUserIdOverride) {
|
|
|
|
qs.append('user_id', client.applicationServiceUserIdOverride);
|
2022-06-09 19:44:57 -06:00
|
|
|
}
|
|
|
|
|
2022-08-29 17:56:31 -06:00
|
|
|
const joinRoomUrl = urlJoin(
|
|
|
|
client.homeserverUrl,
|
2022-10-27 00:09:13 -06:00
|
|
|
`/_matrix/client/v3/join/${encodeURIComponent(roomId)}?${qs.toString()}`
|
2022-06-09 19:44:57 -06:00
|
|
|
);
|
2022-11-03 03:12:00 -06:00
|
|
|
const { data: joinRoomResponse } = await fetchEndpointAsJson(joinRoomUrl, {
|
2022-08-29 17:56:31 -06:00
|
|
|
method: 'POST',
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
});
|
2022-06-09 19:44:57 -06:00
|
|
|
|
|
|
|
const joinedRoomId = joinRoomResponse['room_id'];
|
|
|
|
assert(joinedRoomId);
|
|
|
|
return joinedRoomId;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function sendMessage({ client, roomId, content, timestamp }) {
|
|
|
|
return sendEvent({ client, roomId, eventType: 'm.room.message', content, timestamp });
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a number of messages in the given room
|
2023-04-05 03:25:31 -06:00
|
|
|
async function createMessagesInRoom({
|
|
|
|
client,
|
|
|
|
roomId,
|
|
|
|
numMessages,
|
|
|
|
prefix,
|
|
|
|
timestamp,
|
|
|
|
// The amount of time between each message
|
|
|
|
increment = 1,
|
|
|
|
}) {
|
2022-06-09 19:44:57 -06:00
|
|
|
let eventIds = [];
|
2023-04-05 03:25:31 -06:00
|
|
|
let eventMap = new Map();
|
2022-06-09 19:44:57 -06:00
|
|
|
for (let i = 0; i < numMessages; i++) {
|
2023-04-05 03:25:31 -06:00
|
|
|
const originServerTs = timestamp + i * increment;
|
|
|
|
const content = {
|
|
|
|
msgtype: 'm.text',
|
|
|
|
body: `${prefix} - message${i}`,
|
|
|
|
};
|
2022-06-09 19:44:57 -06:00
|
|
|
const eventId = await sendMessage({
|
|
|
|
client,
|
|
|
|
roomId,
|
2023-04-05 03:25:31 -06:00
|
|
|
content,
|
|
|
|
// Technically, we don't have to set the timestamp to be unique or sequential but
|
|
|
|
// it still seems like a good idea to make the tests more clear.
|
|
|
|
timestamp: originServerTs,
|
2022-06-09 19:44:57 -06:00
|
|
|
});
|
|
|
|
eventIds.push(eventId);
|
2023-04-05 03:25:31 -06:00
|
|
|
eventMap.set(eventId, {
|
|
|
|
roomId,
|
|
|
|
originServerTs,
|
|
|
|
content,
|
|
|
|
});
|
2022-06-09 19:44:57 -06:00
|
|
|
}
|
|
|
|
|
2022-08-29 17:56:31 -06:00
|
|
|
// Sanity check that we actually sent some messages
|
|
|
|
assert.strictEqual(eventIds.length, numMessages);
|
|
|
|
|
2023-04-05 03:25:31 -06:00
|
|
|
return { eventIds, eventMap };
|
2022-06-09 19:44:57 -06:00
|
|
|
}
|
|
|
|
|
2023-04-19 00:26:15 -06:00
|
|
|
async function getMessagesInRoom({ client, roomId, limit }) {
|
|
|
|
assert(client);
|
|
|
|
assert(roomId);
|
|
|
|
assert(limit);
|
|
|
|
|
|
|
|
let qs = new URLSearchParams();
|
|
|
|
qs.append('limit', limit);
|
|
|
|
|
|
|
|
const { data } = await fetchEndpointAsJson(
|
|
|
|
urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/messages?${qs.toString()}`
|
|
|
|
),
|
|
|
|
{
|
|
|
|
method: 'GET',
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return data.chunk;
|
|
|
|
}
|
|
|
|
|
2022-06-29 05:56:13 -06:00
|
|
|
async function updateProfile({ client, displayName, avatarUrl }) {
|
|
|
|
let qs = new URLSearchParams();
|
|
|
|
if (client.applicationServiceUserIdOverride) {
|
|
|
|
qs.append('user_id', client.applicationServiceUserIdOverride);
|
|
|
|
}
|
|
|
|
|
2022-08-29 17:56:31 -06:00
|
|
|
let updateDisplayNamePromise = Promise.resolve();
|
|
|
|
if (displayName) {
|
|
|
|
updateDisplayNamePromise = fetchEndpointAsJson(
|
|
|
|
urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`/_matrix/client/v3/profile/${client.userId}/displayname?${qs.toString()}`
|
|
|
|
),
|
|
|
|
{
|
|
|
|
method: 'PUT',
|
|
|
|
body: {
|
|
|
|
displayname: displayName,
|
|
|
|
},
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2022-06-29 05:56:13 -06:00
|
|
|
|
2022-08-29 17:56:31 -06:00
|
|
|
let updateAvatarUrlPromise = Promise.resolve();
|
|
|
|
if (avatarUrl) {
|
|
|
|
updateAvatarUrlPromise = fetchEndpointAsJson(
|
|
|
|
urlJoin(
|
|
|
|
client.homeserverUrl,
|
|
|
|
`/_matrix/client/v3/profile/${client.userId}/avatar_url?${qs.toString()}`
|
|
|
|
),
|
|
|
|
{
|
|
|
|
method: 'PUT',
|
|
|
|
body: {
|
|
|
|
avatar_url: avatarUrl,
|
|
|
|
},
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
2022-06-29 05:56:13 -06:00
|
|
|
|
|
|
|
await Promise.all([updateDisplayNamePromise, updateAvatarUrlPromise]);
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-06-09 19:44:57 -06:00
|
|
|
// Uploads the given data Buffer and returns the MXC URI of the uploaded content
|
|
|
|
async function uploadContent({ client, roomId, data, fileName, contentType }) {
|
|
|
|
assert(client);
|
|
|
|
assert(roomId);
|
|
|
|
assert(data);
|
|
|
|
|
|
|
|
let qs = new URLSearchParams();
|
2022-06-29 05:56:13 -06:00
|
|
|
if (client.applicationServiceUserIdOverride) {
|
|
|
|
qs.append('user_id', client.applicationServiceUserIdOverride);
|
2022-06-09 19:44:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
if (fileName) {
|
|
|
|
qs.append('filename', fileName);
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't want to use `fetchEndpointAsJson` here because it will
|
|
|
|
// `JSON.stringify(...)` the body data
|
|
|
|
const uploadResponse = await fetchEndpoint(
|
|
|
|
urlJoin(client.homeserverUrl, `/_matrix/media/v3/upload`),
|
|
|
|
{
|
|
|
|
method: 'POST',
|
|
|
|
body: data,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': contentType || 'application/octet-stream',
|
|
|
|
},
|
|
|
|
accessToken: client.accessToken,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const uploadResponseData = await uploadResponse.json();
|
|
|
|
|
|
|
|
const mxcUri = uploadResponseData['content_uri'];
|
|
|
|
assert(mxcUri);
|
|
|
|
return mxcUri;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
2022-08-29 17:56:31 -06:00
|
|
|
ensureUserRegistered,
|
|
|
|
getTestClientForAs,
|
2022-06-09 19:44:57 -06:00
|
|
|
getTestClientForHs,
|
|
|
|
createTestRoom,
|
2023-04-19 00:26:15 -06:00
|
|
|
upgradeTestRoom,
|
2022-10-27 00:09:13 -06:00
|
|
|
getCanonicalAlias,
|
2022-06-09 19:44:57 -06:00
|
|
|
joinRoom,
|
|
|
|
sendEvent,
|
|
|
|
sendMessage,
|
|
|
|
createMessagesInRoom,
|
2023-04-19 00:26:15 -06:00
|
|
|
getMessagesInRoom,
|
2022-06-29 05:56:13 -06:00
|
|
|
updateProfile,
|
2022-06-09 19:44:57 -06:00
|
|
|
uploadContent,
|
|
|
|
};
|