Follow tombstone and predecessor history (#167)
Fix https://github.com/matrix-org/matrix-public-archive/issues/59 Other updates: - Update tests to use `/roomid/room1/date/2022/01/03` format instead of trying to retrofit the weird alias stuff on there. Which also makes the fancy to actual URL utilities much more simple. - Update to specify `archiveMessageLimit` in the test case because pages have different number of events depending on if we are against a boundary, hidden events, etc.
This commit is contained in:
parent
6c789eae69
commit
551b4e72d1
|
@ -31,6 +31,7 @@
|
||||||
"url-join": "^4.0.1"
|
"url-join": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
@ -2463,8 +2464,9 @@
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
"supports-color": "^7.1.0"
|
"supports-color": "^7.1.0"
|
||||||
|
@ -5480,8 +5482,9 @@
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
@ -7494,6 +7497,8 @@
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
|
@ -9371,6 +9376,8 @@
|
||||||
},
|
},
|
||||||
"supports-color": {
|
"supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"start": "node server/server.js",
|
"start": "node server/server.js",
|
||||||
"start-dev": "node server/start-dev.js",
|
"start-dev": "node server/start-dev.js",
|
||||||
"test": "npm run mocha -- test/**/*-tests.js --timeout 15000",
|
"test": "npm run mocha -- test/**/*-tests.js --timeout 15000",
|
||||||
"test-e2e-interactive": "npm run mocha -- test/e2e-tests.js --timeout 15000 --interactive",
|
"test-e2e-interactive": "npm run mocha -- test/e2e-tests.js --timeout 15000 --bail --interactive",
|
||||||
"nodemon": "nodemon",
|
"nodemon": "nodemon",
|
||||||
"vite": "vite",
|
"vite": "vite",
|
||||||
"mocha": "mocha",
|
"mocha": "mocha",
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"chalk": "^4.1.2",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
"eslint": "^8.37.0",
|
"eslint": "^8.37.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
|
|
@ -4,89 +4,204 @@ const assert = require('assert');
|
||||||
|
|
||||||
const urlJoin = require('url-join');
|
const urlJoin = require('url-join');
|
||||||
const { fetchEndpointAsJson } = require('../fetch-endpoint');
|
const { fetchEndpointAsJson } = require('../fetch-endpoint');
|
||||||
|
const parseViaServersFromUserInput = require('../parse-via-servers-from-user-input');
|
||||||
const { traceFunction } = require('../../tracing/trace-utilities');
|
const { traceFunction } = require('../../tracing/trace-utilities');
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
const matrixServerUrl = config.get('matrixServerUrl');
|
const matrixServerUrl = config.get('matrixServerUrl');
|
||||||
assert(matrixServerUrl);
|
assert(matrixServerUrl);
|
||||||
|
|
||||||
async function fetchRoomData(accessToken, roomId) {
|
function getStateEndpointForRoomIdAndEventType(roomId, eventType) {
|
||||||
assert(accessToken);
|
return urlJoin(
|
||||||
assert(roomId);
|
matrixServerUrl,
|
||||||
|
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(
|
||||||
|
eventType
|
||||||
|
)}?format=event`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const stateNameEndpoint = urlJoin(
|
// Unfortunately, we can't just get the event ID from the `/state?format=event`
|
||||||
|
// endpoint, so we have to do this trick. Related to
|
||||||
|
// https://github.com/matrix-org/synapse/issues/15454
|
||||||
|
//
|
||||||
|
// TODO: Remove this when we have MSC3999 (because it's the only usage)
|
||||||
|
const removeMe_fetchRoomCreateEventId = traceFunction(async function (matrixAccessToken, roomId) {
|
||||||
|
const { data } = await fetchEndpointAsJson(
|
||||||
|
urlJoin(
|
||||||
matrixServerUrl,
|
matrixServerUrl,
|
||||||
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.name`
|
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/messages?dir=f&limit1`
|
||||||
);
|
),
|
||||||
const canoncialAliasEndpoint = urlJoin(
|
{
|
||||||
matrixServerUrl,
|
accessToken: matrixAccessToken,
|
||||||
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.canonical_alias`
|
}
|
||||||
);
|
|
||||||
const stateAvatarEndpoint = urlJoin(
|
|
||||||
matrixServerUrl,
|
|
||||||
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.avatar`
|
|
||||||
);
|
|
||||||
const stateHistoryVisibilityEndpoint = urlJoin(
|
|
||||||
matrixServerUrl,
|
|
||||||
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.history_visibility`
|
|
||||||
);
|
|
||||||
const stateJoinRulesEndpoint = urlJoin(
|
|
||||||
matrixServerUrl,
|
|
||||||
`_matrix/client/r0/rooms/${encodeURIComponent(roomId)}/state/m.room.join_rules`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const roomCreateEventId = data?.chunk?.[0]?.event_id;
|
||||||
|
|
||||||
|
return roomCreateEventId;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRoomCreationInfo = traceFunction(async function (matrixAccessToken, roomId) {
|
||||||
|
const [stateCreateResDataOutcome] = await Promise.allSettled([
|
||||||
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.create'), {
|
||||||
|
accessToken: matrixAccessToken,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let roomCreationTs;
|
||||||
|
let predecessorRoomId;
|
||||||
|
let predecessorLastKnownEventId;
|
||||||
|
if (stateCreateResDataOutcome.reason === undefined) {
|
||||||
|
const { data } = stateCreateResDataOutcome.value;
|
||||||
|
roomCreationTs = data?.origin_server_ts;
|
||||||
|
predecessorLastKnownEventId = data?.content?.event_id;
|
||||||
|
predecessorRoomId = data?.content?.predecessor?.room_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { roomCreationTs, predecessorRoomId, predecessorLastKnownEventId };
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchPredecessorInfo = traceFunction(async function (matrixAccessToken, roomId) {
|
||||||
|
const [roomCreationInfoOutcome, statePredecessorResDataOutcome] = await Promise.allSettled([
|
||||||
|
fetchRoomCreationInfo(matrixAccessToken, roomId),
|
||||||
|
fetchEndpointAsJson(
|
||||||
|
getStateEndpointForRoomIdAndEventType(roomId, 'org.matrix.msc3946.room_predecessor'),
|
||||||
|
{
|
||||||
|
accessToken: matrixAccessToken,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let predecessorRoomId;
|
||||||
|
let predecessorLastKnownEventId;
|
||||||
|
let predecessorViaServers;
|
||||||
|
// Prefer the dynamic predecessor from the dedicated state event
|
||||||
|
if (statePredecessorResDataOutcome.reason === undefined) {
|
||||||
|
const { data } = statePredecessorResDataOutcome.value;
|
||||||
|
predecessorRoomId = data?.content?.predecessor_room_id;
|
||||||
|
predecessorLastKnownEventId = data?.content?.last_known_event_id;
|
||||||
|
predecessorViaServers = parseViaServersFromUserInput(data?.content?.via_servers);
|
||||||
|
}
|
||||||
|
// Then fallback to the predecessor defined by the room creation event
|
||||||
|
else if (roomCreationInfoOutcome.reason === undefined) {
|
||||||
|
({ predecessorRoomId, predecessorLastKnownEventId } = roomCreationInfoOutcome.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roomCreationTs: currentRoomCreationTs } = roomCreationInfoOutcome;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// This is prefixed with "current" so we don't get this confused with the
|
||||||
|
// predecessor room creation timestamp.
|
||||||
|
currentRoomCreationTs,
|
||||||
|
predecessorRoomId,
|
||||||
|
predecessorLastKnownEventId,
|
||||||
|
predecessorViaServers,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchSuccessorInfo = traceFunction(async function (matrixAccessToken, roomId) {
|
||||||
|
const [stateTombstoneResDataOutcome] = await Promise.allSettled([
|
||||||
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.tombstone'), {
|
||||||
|
accessToken: matrixAccessToken,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let successorRoomId;
|
||||||
|
let successorSetTs;
|
||||||
|
if (stateTombstoneResDataOutcome.reason === undefined) {
|
||||||
|
const { data } = stateTombstoneResDataOutcome.value;
|
||||||
|
successorRoomId = data?.content?.replacement_room;
|
||||||
|
successorSetTs = data?.origin_server_ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
successorRoomId,
|
||||||
|
successorSetTs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-statements
|
||||||
|
const fetchRoomData = traceFunction(async function (matrixAccessToken, roomId) {
|
||||||
|
assert(matrixAccessToken);
|
||||||
|
assert(roomId);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
stateNameResDataOutcome,
|
stateNameResDataOutcome,
|
||||||
stateCanonicalAliasResDataOutcome,
|
stateCanonicalAliasResDataOutcome,
|
||||||
stateAvatarResDataOutcome,
|
stateAvatarResDataOutcome,
|
||||||
stateHistoryVisibilityResDataOutcome,
|
stateHistoryVisibilityResDataOutcome,
|
||||||
stateJoinRulesResDataOutcome,
|
stateJoinRulesResDataOutcome,
|
||||||
|
predecessorInfoOutcome,
|
||||||
|
successorInfoOutcome,
|
||||||
] = await Promise.allSettled([
|
] = await Promise.allSettled([
|
||||||
fetchEndpointAsJson(stateNameEndpoint, {
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.name'), {
|
||||||
accessToken,
|
accessToken: matrixAccessToken,
|
||||||
}),
|
}),
|
||||||
fetchEndpointAsJson(canoncialAliasEndpoint, {
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.canonical_alias'), {
|
||||||
accessToken,
|
accessToken: matrixAccessToken,
|
||||||
}),
|
}),
|
||||||
fetchEndpointAsJson(stateAvatarEndpoint, {
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.avatar'), {
|
||||||
accessToken,
|
accessToken: matrixAccessToken,
|
||||||
}),
|
}),
|
||||||
fetchEndpointAsJson(stateHistoryVisibilityEndpoint, {
|
fetchEndpointAsJson(
|
||||||
accessToken,
|
getStateEndpointForRoomIdAndEventType(roomId, 'm.room.history_visibility'),
|
||||||
}),
|
{
|
||||||
fetchEndpointAsJson(stateJoinRulesEndpoint, {
|
accessToken: matrixAccessToken,
|
||||||
accessToken,
|
}
|
||||||
|
),
|
||||||
|
fetchEndpointAsJson(getStateEndpointForRoomIdAndEventType(roomId, 'm.room.join_rules'), {
|
||||||
|
accessToken: matrixAccessToken,
|
||||||
}),
|
}),
|
||||||
|
fetchPredecessorInfo(matrixAccessToken, roomId),
|
||||||
|
fetchSuccessorInfo(matrixAccessToken, roomId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let name;
|
let name;
|
||||||
if (stateNameResDataOutcome.reason === undefined) {
|
if (stateNameResDataOutcome.reason === undefined) {
|
||||||
const { data } = stateNameResDataOutcome.value;
|
const { data } = stateNameResDataOutcome.value;
|
||||||
name = data.name;
|
name = data?.content?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
let canonicalAlias;
|
let canonicalAlias;
|
||||||
if (stateCanonicalAliasResDataOutcome.reason === undefined) {
|
if (stateCanonicalAliasResDataOutcome.reason === undefined) {
|
||||||
const { data } = stateCanonicalAliasResDataOutcome.value;
|
const { data } = stateCanonicalAliasResDataOutcome.value;
|
||||||
canonicalAlias = data.alias;
|
canonicalAlias = data?.content?.alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatarUrl;
|
let avatarUrl;
|
||||||
if (stateAvatarResDataOutcome.reason === undefined) {
|
if (stateAvatarResDataOutcome.reason === undefined) {
|
||||||
const { data } = stateAvatarResDataOutcome.value;
|
const { data } = stateAvatarResDataOutcome.value;
|
||||||
avatarUrl = data.url;
|
avatarUrl = data?.content?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
let historyVisibility;
|
let historyVisibility;
|
||||||
if (stateHistoryVisibilityResDataOutcome.reason === undefined) {
|
if (stateHistoryVisibilityResDataOutcome.reason === undefined) {
|
||||||
const { data } = stateHistoryVisibilityResDataOutcome.value;
|
const { data } = stateHistoryVisibilityResDataOutcome.value;
|
||||||
historyVisibility = data.history_visibility;
|
historyVisibility = data?.content?.history_visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
let joinRule;
|
let joinRule;
|
||||||
if (stateJoinRulesResDataOutcome.reason === undefined) {
|
if (stateJoinRulesResDataOutcome.reason === undefined) {
|
||||||
const { data } = stateJoinRulesResDataOutcome.value;
|
const { data } = stateJoinRulesResDataOutcome.value;
|
||||||
joinRule = data.join_rule;
|
joinRule = data?.content?.join_rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
let roomCreationTs;
|
||||||
|
let predecessorRoomId;
|
||||||
|
let predecessorLastKnownEventId;
|
||||||
|
let predecessorViaServers;
|
||||||
|
if (predecessorInfoOutcome.reason === undefined) {
|
||||||
|
({
|
||||||
|
currentRoomCreationTs: roomCreationTs,
|
||||||
|
predecessorRoomId,
|
||||||
|
predecessorLastKnownEventId,
|
||||||
|
predecessorViaServers,
|
||||||
|
} = predecessorInfoOutcome.value);
|
||||||
|
}
|
||||||
|
let successorRoomId;
|
||||||
|
let successorSetTs;
|
||||||
|
if (successorInfoOutcome.reason === undefined) {
|
||||||
|
({ successorRoomId, successorSetTs } = successorInfoOutcome.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -96,7 +211,19 @@ async function fetchRoomData(accessToken, roomId) {
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
historyVisibility,
|
historyVisibility,
|
||||||
joinRule,
|
joinRule,
|
||||||
|
roomCreationTs,
|
||||||
|
predecessorRoomId,
|
||||||
|
predecessorLastKnownEventId,
|
||||||
|
predecessorViaServers,
|
||||||
|
successorRoomId,
|
||||||
|
successorSetTs,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
module.exports = traceFunction(fetchRoomData);
|
module.exports = {
|
||||||
|
fetchRoomData,
|
||||||
|
fetchRoomCreationInfo,
|
||||||
|
fetchPredecessorInfo,
|
||||||
|
fetchSuccessorInfo,
|
||||||
|
removeMe_fetchRoomCreateEventId,
|
||||||
|
};
|
||||||
|
|
|
@ -4,6 +4,9 @@ const assert = require('assert');
|
||||||
|
|
||||||
// See https://spec.matrix.org/v1.5/appendices/#server-name
|
// See https://spec.matrix.org/v1.5/appendices/#server-name
|
||||||
function getServerNameFromMatrixRoomIdOrAlias(roomIdOrAlias) {
|
function getServerNameFromMatrixRoomIdOrAlias(roomIdOrAlias) {
|
||||||
|
// `roomIdOrAlias` looks like `!foo:matrix.org` or `#foo:matrix.org` or even something
|
||||||
|
// as crazy as `!foo:[1234:5678::abcd]:1234` where `[1234:5678::abcd]:1234` is the
|
||||||
|
// server name part we're trying to parse out (see tests for more examples)
|
||||||
assert(roomIdOrAlias);
|
assert(roomIdOrAlias);
|
||||||
|
|
||||||
const pieces = roomIdOrAlias.split(':');
|
const pieces = roomIdOrAlias.split(':');
|
||||||
|
|
|
@ -12,10 +12,15 @@ const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-arc
|
||||||
|
|
||||||
const { HTTPResponseError } = require('../lib/fetch-endpoint');
|
const { HTTPResponseError } = require('../lib/fetch-endpoint');
|
||||||
const parseViaServersFromUserInput = require('../lib/parse-via-servers-from-user-input');
|
const parseViaServersFromUserInput = require('../lib/parse-via-servers-from-user-input');
|
||||||
const fetchRoomData = require('../lib/matrix-utils/fetch-room-data');
|
const {
|
||||||
|
fetchRoomData,
|
||||||
|
fetchPredecessorInfo,
|
||||||
|
fetchSuccessorInfo,
|
||||||
|
} = require('../lib/matrix-utils/fetch-room-data');
|
||||||
const fetchEventsFromTimestampBackwards = require('../lib/matrix-utils/fetch-events-from-timestamp-backwards');
|
const fetchEventsFromTimestampBackwards = require('../lib/matrix-utils/fetch-events-from-timestamp-backwards');
|
||||||
const ensureRoomJoined = require('../lib/matrix-utils/ensure-room-joined');
|
const ensureRoomJoined = require('../lib/matrix-utils/ensure-room-joined');
|
||||||
const timestampToEvent = require('../lib/matrix-utils/timestamp-to-event');
|
const timestampToEvent = require('../lib/matrix-utils/timestamp-to-event');
|
||||||
|
const { removeMe_fetchRoomCreateEventId } = require('../lib/matrix-utils/fetch-room-data');
|
||||||
const getMessagesResponseFromEventId = require('../lib/matrix-utils/get-messages-response-from-event-id');
|
const getMessagesResponseFromEventId = require('../lib/matrix-utils/get-messages-response-from-event-id');
|
||||||
const renderHydrogenVmRenderScriptToPageHtml = require('../hydrogen-render/render-hydrogen-vm-render-script-to-page-html');
|
const renderHydrogenVmRenderScriptToPageHtml = require('../hydrogen-render/render-hydrogen-vm-render-script-to-page-html');
|
||||||
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
|
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
|
||||||
|
@ -23,6 +28,7 @@ const {
|
||||||
MS_LOOKUP,
|
MS_LOOKUP,
|
||||||
TIME_PRECISION_VALUES,
|
TIME_PRECISION_VALUES,
|
||||||
DIRECTION,
|
DIRECTION,
|
||||||
|
VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP,
|
||||||
} = require('matrix-public-archive-shared/lib/reference-values');
|
} = require('matrix-public-archive-shared/lib/reference-values');
|
||||||
const { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, ONE_MINUTE_IN_MS, ONE_SECOND_IN_MS } = MS_LOOKUP;
|
const { ONE_DAY_IN_MS, ONE_HOUR_IN_MS, ONE_MINUTE_IN_MS, ONE_SECOND_IN_MS } = MS_LOOKUP;
|
||||||
const {
|
const {
|
||||||
|
@ -57,10 +63,6 @@ const router = express.Router({
|
||||||
mergeParams: true,
|
mergeParams: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP = {
|
|
||||||
r: '#',
|
|
||||||
roomid: '!',
|
|
||||||
};
|
|
||||||
const validSigilList = Object.values(VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP);
|
const validSigilList = Object.values(VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP);
|
||||||
const sigilRe = new RegExp(`^(${validSigilList.join('|')})`);
|
const sigilRe = new RegExp(`^(${validSigilList.join('|')})`);
|
||||||
|
|
||||||
|
@ -231,31 +233,46 @@ router.get(
|
||||||
'?dir query parameter must be [f|b]'
|
'?dir query parameter must be [f|b]'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timelineStartEventId = req.query.timelineStartEventId;
|
||||||
|
assert(
|
||||||
|
['string', 'undefined'].includes(typeof timelineStartEventId),
|
||||||
|
`?timelineStartEventId must be a string or undefined but saw ${typeof timelineStartEventId}`
|
||||||
|
);
|
||||||
|
const timelineEndEventId = req.query.timelineEndEventId;
|
||||||
|
assert(
|
||||||
|
['string', 'undefined'].includes(typeof timelineStartEventId),
|
||||||
|
`?timelineEndEventId must be a string or undefined but saw ${typeof timelineStartEventId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// We have to wait for the room join to happen first before we can use the jump to
|
||||||
|
// date endpoint (or any other Matrix endpoint)
|
||||||
|
const viaServers = parseViaServersFromUserInput(req.query.via);
|
||||||
|
const roomId = await ensureRoomJoined(matrixAccessToken, roomIdOrAlias, viaServers);
|
||||||
|
|
||||||
let ts;
|
let ts;
|
||||||
|
let fromCausalEventId;
|
||||||
if (dir === DIRECTION.backward) {
|
if (dir === DIRECTION.backward) {
|
||||||
// We `- 1` so we don't jump to the same event because the endpoint is inclusive.
|
// We `- 1` so we don't jump to the same event because the endpoint is inclusive.
|
||||||
//
|
//
|
||||||
// XXX: This is probably an edge-case flaw when there could be multiple events at
|
// XXX: This is probably an edge-case flaw when there could be multiple events at
|
||||||
// the same timestamp
|
// the same timestamp
|
||||||
|
//
|
||||||
|
// TODO: Remove the `- 1` when we have the MSC3999 causal event ID support
|
||||||
ts = currentRangeStartTs - 1;
|
ts = currentRangeStartTs - 1;
|
||||||
|
fromCausalEventId = timelineStartEventId;
|
||||||
} else if (dir === DIRECTION.forward) {
|
} else if (dir === DIRECTION.forward) {
|
||||||
// We `+ 1` so we don't jump to the same event because the endpoint is inclusive
|
// We `+ 1` so we don't jump to the same event because the endpoint is inclusive
|
||||||
//
|
//
|
||||||
// XXX: This is probably an edge-case flaw when there could be multiple events at
|
// XXX: This is probably an edge-case flaw when there could be multiple events at
|
||||||
// the same timestamp
|
// the same timestamp
|
||||||
|
//
|
||||||
|
// TODO: Remove the `+ 1` when we have the MSC3999 causal event ID support
|
||||||
ts = currentRangeEndTs + 1;
|
ts = currentRangeEndTs + 1;
|
||||||
|
fromCausalEventId = timelineEndEventId;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unable to handle unknown dir=${dir} in /jump`);
|
throw new StatusError(400, `Unable to handle unknown dir=${dir} in /jump`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to wait for the room join to happen first before we can use the jump to
|
|
||||||
// date endpoint
|
|
||||||
const roomId = await ensureRoomJoined(
|
|
||||||
matrixAccessToken,
|
|
||||||
roomIdOrAlias,
|
|
||||||
parseViaServersFromUserInput(req.query.via)
|
|
||||||
);
|
|
||||||
|
|
||||||
let eventIdForClosestEvent;
|
let eventIdForClosestEvent;
|
||||||
let tsForClosestEvent;
|
let tsForClosestEvent;
|
||||||
let newOriginServerTs;
|
let newOriginServerTs;
|
||||||
|
@ -265,14 +282,53 @@ router.get(
|
||||||
// updated value between each e2e test
|
// updated value between each e2e test
|
||||||
const archiveMessageLimit = config.get('archiveMessageLimit');
|
const archiveMessageLimit = config.get('archiveMessageLimit');
|
||||||
|
|
||||||
|
let roomCreateEventId;
|
||||||
// Find the closest event to the given timestamp
|
// Find the closest event to the given timestamp
|
||||||
({ eventId: eventIdForClosestEvent, originServerTs: tsForClosestEvent } =
|
[{ eventId: eventIdForClosestEvent, originServerTs: tsForClosestEvent }, roomCreateEventId] =
|
||||||
await timestampToEvent({
|
await Promise.all([
|
||||||
|
timestampToEvent({
|
||||||
accessToken: matrixAccessToken,
|
accessToken: matrixAccessToken,
|
||||||
roomId,
|
roomId,
|
||||||
ts: ts,
|
ts: ts,
|
||||||
direction: dir,
|
direction: dir,
|
||||||
}));
|
// Since timestamps are untrusted and can be crafted to make loops in the
|
||||||
|
// timeline. We use this as a signal to keep progressing from this event
|
||||||
|
// regardless of what timestamp shenanigans are going on. See MSC3999
|
||||||
|
// (https://github.com/matrix-org/matrix-spec-proposals/pull/3999)
|
||||||
|
//
|
||||||
|
// TODO: Add tests for timestamp loops once Synapse supports MSC3999. We
|
||||||
|
// currently just have this set in case some server has this implemented in
|
||||||
|
// the future but there currently is no implementation (as of 2023-04-17) and
|
||||||
|
// we can't have passing tests without a server implementation first.
|
||||||
|
'org.matrix.msc3999.event_id': fromCausalEventId,
|
||||||
|
}),
|
||||||
|
removeMe_fetchRoomCreateEventId(matrixAccessToken, roomId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Without MSC3999, we currently only detect one kind of loop where the
|
||||||
|
// `m.room.create` has a timestamp that comes after the timestamp massaged events
|
||||||
|
// in the room. This is a common pattern for historical Gitter rooms where we
|
||||||
|
// created the room and then imported a bunch of messages at a time before the
|
||||||
|
// room was created.
|
||||||
|
//
|
||||||
|
// By nature of having an `timelineEndEventId`, we know we are already paginated
|
||||||
|
// past the `m.room.create` event which is always the first event in the room. So
|
||||||
|
// we can use that to detect the end of the room before we loop back around to the
|
||||||
|
// start of the room.
|
||||||
|
//
|
||||||
|
// XXX: Once we have MSC3999, we can remove this check in favor of that mechanism
|
||||||
|
if (
|
||||||
|
dir === DIRECTION.forward &&
|
||||||
|
timelineEndEventId &&
|
||||||
|
eventIdForClosestEvent === roomCreateEventId
|
||||||
|
) {
|
||||||
|
throw new StatusError(
|
||||||
|
404,
|
||||||
|
`/jump?dir=${dir}: We detected a loop back to the beginning of the room so we can assume ` +
|
||||||
|
`we hit the end of the room instead of doing a loop. We throw a 404 error here we hit ` +
|
||||||
|
`the normal 404 no more /messages error handling below`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Based on what we found was the closest, figure out the URL that will represent
|
// Based on what we found was the closest, figure out the URL that will represent
|
||||||
// the next chunk in the desired direction.
|
// the next chunk in the desired direction.
|
||||||
|
@ -280,24 +336,31 @@ router.get(
|
||||||
//
|
//
|
||||||
// When jumping backwards, since a given room archive URL represents the end of
|
// When jumping backwards, since a given room archive URL represents the end of
|
||||||
// the day/time-period looking backward (scroll is also anchored to the bottom),
|
// the day/time-period looking backward (scroll is also anchored to the bottom),
|
||||||
// we just need to get the user to the previous time-period.
|
// we just need to move the user to the time-period just prior the current one.
|
||||||
//
|
//
|
||||||
// We are trying to avoid sending the user to the same time period they were just
|
// We are trying to avoid sending the user to the same time period they were just
|
||||||
// viewing. i.e, if they were visiting `/2020/01/02T16:00:00` (displays messages
|
// viewing. i.e, if they were visiting `/2020/01/02T16:00:00` (displays messages
|
||||||
// backwards from that time up to the limit), which had more messages than we
|
// backwards from that time up to the limit), which had more messages than we
|
||||||
// could display in that day, jumping backwards from the earliest displayed event
|
// could display in that day, jumping backwards from the earliest displayed event
|
||||||
// in the displayed range, say `T12:00:05` would still give us the same day
|
// in the displayed range (say that occured on `T12:00:25`) would still give us
|
||||||
// `/2020/01/02` and we want to redirect them to previous chunk from that same
|
// the same day `/2020/01/02` and we want to redirect them to previous chunk from
|
||||||
// day, like `/2020/01/02T12:00:00`
|
// that same day that still encompasses the closest message looking backwards,
|
||||||
|
// like `/2020/01/02T13:00:00`
|
||||||
if (dir === DIRECTION.backward) {
|
if (dir === DIRECTION.backward) {
|
||||||
|
// We choose `currentRangeStartTs` instead of `ts` (the jump point) because
|
||||||
|
// TODO: why? and we don't choose `currentRangeEndTs` because TODO: why? - I
|
||||||
|
// feel like I can't justify this, see
|
||||||
|
// https://github.com/matrix-org/matrix-public-archive/pull/167#discussion_r1170850432
|
||||||
const fromSameDay =
|
const fromSameDay =
|
||||||
tsForClosestEvent && areTimestampsFromSameUtcDay(currentRangeEndTs, tsForClosestEvent);
|
tsForClosestEvent && areTimestampsFromSameUtcDay(currentRangeStartTs, tsForClosestEvent);
|
||||||
const fromSameHour =
|
const fromSameHour =
|
||||||
tsForClosestEvent && areTimestampsFromSameUtcHour(currentRangeEndTs, tsForClosestEvent);
|
tsForClosestEvent && areTimestampsFromSameUtcHour(currentRangeStartTs, tsForClosestEvent);
|
||||||
const fromSameMinute =
|
const fromSameMinute =
|
||||||
tsForClosestEvent && areTimestampsFromSameUtcMinute(currentRangeEndTs, tsForClosestEvent);
|
tsForClosestEvent &&
|
||||||
|
areTimestampsFromSameUtcMinute(currentRangeStartTs, tsForClosestEvent);
|
||||||
const fromSameSecond =
|
const fromSameSecond =
|
||||||
tsForClosestEvent && areTimestampsFromSameUtcSecond(currentRangeEndTs, tsForClosestEvent);
|
tsForClosestEvent &&
|
||||||
|
areTimestampsFromSameUtcSecond(currentRangeStartTs, tsForClosestEvent);
|
||||||
|
|
||||||
// The closest event is from the same second we tried to jump from. Since we
|
// The closest event is from the same second we tried to jump from. Since we
|
||||||
// can't represent something smaller than a second in the URL yet (we could do
|
// can't represent something smaller than a second in the URL yet (we could do
|
||||||
|
@ -355,13 +418,11 @@ router.get(
|
||||||
// XXX: This is flawed in the fact that when we go `/messages?dir=b` later, it
|
// XXX: This is flawed in the fact that when we go `/messages?dir=b` later, it
|
||||||
// could backfill messages which will fill up the response before we perfectly
|
// could backfill messages which will fill up the response before we perfectly
|
||||||
// connect and continue from the position they were jumping from before. When
|
// connect and continue from the position they were jumping from before. When
|
||||||
// `/messages?dir=f` backfills, we won't have this problem anymore because any
|
// `/messages?dir=f` backfills (forwards fill), we won't have this problem anymore
|
||||||
// messages backfilled in the forwards direction would be picked up the same going
|
// because any messages backfilled in the forwards direction would be picked up
|
||||||
// backwards.
|
// the same going backwards. See MSC4000
|
||||||
if (dir === DIRECTION.forward) {
|
// (https://github.com/matrix-org/matrix-spec-proposals/pull/4000).
|
||||||
// Use `/messages?dir=f` and get the `end` pagination token to paginate from. And
|
else if (dir === DIRECTION.forward) {
|
||||||
// then start the scroll from the top of the page so they can continue.
|
|
||||||
//
|
|
||||||
// XXX: It would be cool to somehow cache this response and re-use our work here
|
// XXX: It would be cool to somehow cache this response and re-use our work here
|
||||||
// for the actual room display that we redirect to from this route. No need for
|
// for the actual room display that we redirect to from this route. No need for
|
||||||
// us go out 100 messages, only for us to go backwards 100 messages again in the
|
// us go out 100 messages, only for us to go backwards 100 messages again in the
|
||||||
|
@ -377,13 +438,50 @@ router.get(
|
||||||
if (!messageResData.chunk?.length) {
|
if (!messageResData.chunk?.length) {
|
||||||
throw new StatusError(
|
throw new StatusError(
|
||||||
404,
|
404,
|
||||||
`/jump?dir=${dir}: /messages response didn't contain any more messages to jump to`
|
`/jump?dir=${dir}: /messages response didn't contain any more messages to jump to so we can assume we reached the end of the room.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestampOfLastMessage =
|
const firstMessage = messageResData.chunk[0];
|
||||||
messageResData.chunk[messageResData.chunk.length - 1].origin_server_ts;
|
const tsOfFirstMessage = firstMessage.origin_server_ts;
|
||||||
const dateOfLastMessage = new Date(timestampOfLastMessage);
|
|
||||||
|
const lastMessage = messageResData.chunk[messageResData.chunk.length - 1];
|
||||||
|
const tsOfLastMessage = lastMessage.origin_server_ts;
|
||||||
|
|
||||||
|
let msGapFromJumpPointToLastMessage;
|
||||||
|
// If someone is jumping from `0`, let's assume this is their first time
|
||||||
|
// navigating in the room and are just trying to get to the first messages in
|
||||||
|
// the room. Instead of using `0` which give us `moreThanDayGap=true` every time
|
||||||
|
// (unless someone sent messages in 1970 :P), and round us down to the nearest
|
||||||
|
// day before any of the messages in the room start, let's just use the start of
|
||||||
|
// the timeline as the start which will show us a page of content on the first
|
||||||
|
// try. For the backwards direction, we could have a similar check but with
|
||||||
|
// `currentRangeStartTs === Infinity` check but it's not necessary since we
|
||||||
|
// don't have to do any back-tracking extra work.
|
||||||
|
if (currentRangeEndTs === 0) {
|
||||||
|
msGapFromJumpPointToLastMessage = tsOfLastMessage - tsOfFirstMessage;
|
||||||
|
}
|
||||||
|
// Otherwise do the normal calculation: where we jumped to - where we jumped from
|
||||||
|
else {
|
||||||
|
// TODO: Should we use `ts` or `currentRangeStartTs` here?
|
||||||
|
msGapFromJumpPointToLastMessage = tsOfLastMessage - ts;
|
||||||
|
}
|
||||||
|
const moreThanDayGap = msGapFromJumpPointToLastMessage > ONE_DAY_IN_MS;
|
||||||
|
const moreThanHourGap = msGapFromJumpPointToLastMessage > ONE_HOUR_IN_MS;
|
||||||
|
const moreThanMinuteGap = msGapFromJumpPointToLastMessage > ONE_MINUTE_IN_MS;
|
||||||
|
const moreThanSecondGap = msGapFromJumpPointToLastMessage > ONE_SECOND_IN_MS;
|
||||||
|
|
||||||
|
// If the first message is on different day than the last message, then we know
|
||||||
|
// there are messages on days before the last mesage and can safely round to the
|
||||||
|
// nearest day and still see new content.
|
||||||
|
//
|
||||||
|
// We use this information to handle situations where we jump over multiple-day
|
||||||
|
// gaps with no messages in between. In those cases, we don't want to round down
|
||||||
|
// to a day where there are no messages in the gap.
|
||||||
|
const hasMessagesOnDayBeforeDayOfLastMessage = !areTimestampsFromSameUtcDay(
|
||||||
|
tsOfFirstMessage,
|
||||||
|
tsOfLastMessage
|
||||||
|
);
|
||||||
|
|
||||||
// Back-track from the last message timestamp to the nearest date boundary.
|
// Back-track from the last message timestamp to the nearest date boundary.
|
||||||
// Because we're back-tracking a couple events here, when we paginate back out
|
// Because we're back-tracking a couple events here, when we paginate back out
|
||||||
|
@ -395,29 +493,25 @@ router.get(
|
||||||
// back-tracking but then we get ugly URL's every time you jump instead of being
|
// back-tracking but then we get ugly URL's every time you jump instead of being
|
||||||
// able to back-track and round down to the nearest hour in a lot of cases. The
|
// able to back-track and round down to the nearest hour in a lot of cases. The
|
||||||
// other reason not to return the exact date is maybe there multiple messages at
|
// other reason not to return the exact date is maybe there multiple messages at
|
||||||
// the same timestamp and we will lose messages in the gap it displays more than
|
// the same timestamp and we will lose messages in the gap because it displays
|
||||||
// we thought.
|
// more than we thought.
|
||||||
const msGapFromJumpPointToLastMessage = timestampOfLastMessage - ts;
|
//
|
||||||
const moreThanDayGap = msGapFromJumpPointToLastMessage > ONE_DAY_IN_MS;
|
|
||||||
const moreThanHourGap = msGapFromJumpPointToLastMessage > ONE_HOUR_IN_MS;
|
|
||||||
const moreThanMinuteGap = msGapFromJumpPointToLastMessage > ONE_MINUTE_IN_MS;
|
|
||||||
const moreThanSecondGap = msGapFromJumpPointToLastMessage > ONE_SECOND_IN_MS;
|
|
||||||
|
|
||||||
// If the `/messages` response returns less than the `archiveMessageLimit`
|
// If the `/messages` response returns less than the `archiveMessageLimit`
|
||||||
// looking forwards, it means we're looking at the latest events in the room. We
|
// looking forwards, it means we're looking at the latest events in the room. We
|
||||||
// can simply just display the day that the latest event occured on or given
|
// can simply just display the day that the latest event occured on or the given
|
||||||
// rangeEnd (whichever is later).
|
// rangeEnd (whichever is later).
|
||||||
const haveReachedLatestMessagesInRoom = messageResData.chunk?.length < archiveMessageLimit;
|
const haveReachedLatestMessagesInRoom = messageResData.chunk?.length < archiveMessageLimit;
|
||||||
if (haveReachedLatestMessagesInRoom) {
|
if (haveReachedLatestMessagesInRoom) {
|
||||||
const latestDesiredTs = Math.max(currentRangeEndTs, timestampOfLastMessage);
|
const latestDesiredTs = Math.max(currentRangeEndTs, tsOfLastMessage);
|
||||||
const latestDesiredDate = new Date(latestDesiredTs);
|
const latestDesiredDate = new Date(latestDesiredTs);
|
||||||
const utcMidnightTs = getUtcStartOfDayTs(latestDesiredDate);
|
const utcMidnightTs = getUtcStartOfDayTs(latestDesiredDate);
|
||||||
newOriginServerTs = utcMidnightTs;
|
newOriginServerTs = utcMidnightTs;
|
||||||
preferredPrecision = TIME_PRECISION_VALUES.none;
|
preferredPrecision = TIME_PRECISION_VALUES.none;
|
||||||
}
|
}
|
||||||
// More than a day gap here, so we can just back-track to the nearest day
|
// More than a day gap here, so we can just back-track to the nearest day as
|
||||||
else if (moreThanDayGap) {
|
// long as there are messages we haven't seen yet if we visit the nearest day.
|
||||||
const utcMidnightOfDayBefore = getUtcStartOfDayTs(dateOfLastMessage);
|
else if (moreThanDayGap && hasMessagesOnDayBeforeDayOfLastMessage) {
|
||||||
|
const utcMidnightOfDayBefore = getUtcStartOfDayTs(tsOfLastMessage);
|
||||||
// We `- 1` from UTC midnight to get the timestamp that is a millisecond
|
// We `- 1` from UTC midnight to get the timestamp that is a millisecond
|
||||||
// before the next day but we choose a no time precision so we jump to just
|
// before the next day but we choose a no time precision so we jump to just
|
||||||
// the bare date without a time. A bare date in the `/date/2022/12/16`
|
// the bare date without a time. A bare date in the `/date/2022/12/16`
|
||||||
|
@ -429,19 +523,19 @@ router.get(
|
||||||
}
|
}
|
||||||
// More than a hour gap here, we will need to back-track to the nearest hour
|
// More than a hour gap here, we will need to back-track to the nearest hour
|
||||||
else if (moreThanHourGap) {
|
else if (moreThanHourGap) {
|
||||||
const utcTopOfHourBefore = getUtcStartOfHourTs(dateOfLastMessage);
|
const utcTopOfHourBefore = getUtcStartOfHourTs(tsOfLastMessage);
|
||||||
newOriginServerTs = utcTopOfHourBefore;
|
newOriginServerTs = utcTopOfHourBefore;
|
||||||
preferredPrecision = TIME_PRECISION_VALUES.minutes;
|
preferredPrecision = TIME_PRECISION_VALUES.minutes;
|
||||||
}
|
}
|
||||||
// More than a minute gap here, we will need to back-track to the nearest minute
|
// More than a minute gap here, we will need to back-track to the nearest minute
|
||||||
else if (moreThanMinuteGap) {
|
else if (moreThanMinuteGap) {
|
||||||
const utcTopOfMinuteBefore = getUtcStartOfMinuteTs(dateOfLastMessage);
|
const utcTopOfMinuteBefore = getUtcStartOfMinuteTs(tsOfLastMessage);
|
||||||
newOriginServerTs = utcTopOfMinuteBefore;
|
newOriginServerTs = utcTopOfMinuteBefore;
|
||||||
preferredPrecision = TIME_PRECISION_VALUES.minutes;
|
preferredPrecision = TIME_PRECISION_VALUES.minutes;
|
||||||
}
|
}
|
||||||
// More than a second gap here, we will need to back-track to the nearest second
|
// More than a second gap here, we will need to back-track to the nearest second
|
||||||
else if (moreThanSecondGap) {
|
else if (moreThanSecondGap) {
|
||||||
const utcTopOfSecondBefore = getUtcStartOfSecondTs(dateOfLastMessage);
|
const utcTopOfSecondBefore = getUtcStartOfSecondTs(tsOfLastMessage);
|
||||||
newOriginServerTs = utcTopOfSecondBefore;
|
newOriginServerTs = utcTopOfSecondBefore;
|
||||||
preferredPrecision = TIME_PRECISION_VALUES.seconds;
|
preferredPrecision = TIME_PRECISION_VALUES.seconds;
|
||||||
}
|
}
|
||||||
|
@ -459,11 +553,134 @@ router.get(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const is404Error = err instanceof HTTPResponseError && err.response.status === 404;
|
const is404HTTPResponseError =
|
||||||
|
err instanceof HTTPResponseError && err.response.status === 404;
|
||||||
|
const is404StatusError = err instanceof StatusError && err.status === 404;
|
||||||
|
const is404Error = is404HTTPResponseError || is404StatusError;
|
||||||
|
// A 404 error just means there is no more messages to paginate in that room and
|
||||||
|
// we should try to go to the predecessor/successor room appropriately.
|
||||||
|
if (is404Error) {
|
||||||
|
if (dir === DIRECTION.backward) {
|
||||||
|
const {
|
||||||
|
currentRoomCreationTs,
|
||||||
|
predecessorRoomId,
|
||||||
|
predecessorLastKnownEventId,
|
||||||
|
predecessorViaServers,
|
||||||
|
} = await fetchPredecessorInfo(matrixAccessToken, roomId);
|
||||||
|
|
||||||
|
if (!predecessorRoomId) {
|
||||||
|
throw new StatusError(
|
||||||
|
404,
|
||||||
|
`No predecessor room found for ${roomId} so we can't jump backwards to anywhere (you already reached the end of the room)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to join the predecessor room before we can fetch the successor info
|
||||||
|
// (this could be our first time seeing the room)
|
||||||
|
await ensureRoomJoined(matrixAccessToken, predecessorRoomId, viaServers);
|
||||||
|
const {
|
||||||
|
successorRoomId: successorRoomIdForPredecessor,
|
||||||
|
successorSetTs: successorSetTsForPredecessor,
|
||||||
|
} = await fetchSuccessorInfo(matrixAccessToken, predecessorRoomId);
|
||||||
|
|
||||||
|
let tombstoneEventId;
|
||||||
|
if (!predecessorLastKnownEventId) {
|
||||||
|
// This is a hack because we can't get the tombstone event ID directly from
|
||||||
|
// `fetchSuccessorInfo(...)` and the `/state?format=event`
|
||||||
|
// endpoint, so we have to do this trick. Related to
|
||||||
|
// https://github.com/matrix-org/synapse/issues/15454
|
||||||
|
//
|
||||||
|
// We just assume this is the tombstone event ID but in any case it gets us to
|
||||||
|
// an event that happened at the same time.
|
||||||
|
({ eventId: tombstoneEventId } = await timestampToEvent({
|
||||||
|
accessToken: matrixAccessToken,
|
||||||
|
roomId: predecessorRoomId,
|
||||||
|
ts: successorSetTsForPredecessor,
|
||||||
|
direction: DIRECTION.backward,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to continue from the tombstone event in the predecessor room because
|
||||||
|
// that is the signal that the room admins gave to indicate the end of the
|
||||||
|
// room in favor of the other regardless of further activity that may have
|
||||||
|
// occured in the room.
|
||||||
|
//
|
||||||
|
// Make sure the the room that the predecessor specifies as the replacement
|
||||||
|
// room is the same as what the current room is. This is a good signal that
|
||||||
|
// the rooms are a true continuation of each other and the room admins agree.
|
||||||
|
let continueAtTsInPredecessorRoom;
|
||||||
|
if (successorRoomIdForPredecessor === roomId) {
|
||||||
|
continueAtTsInPredecessorRoom = successorSetTsForPredecessor;
|
||||||
|
}
|
||||||
|
// Fallback to the room creation event time if we can't find the predecessor
|
||||||
|
// room tombstone which will work just fine and as expected for normal room
|
||||||
|
// upgrade scenarios.
|
||||||
|
else {
|
||||||
|
continueAtTsInPredecessorRoom = currentRoomCreationTs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
continueAtTsInPredecessorRoom === null ||
|
||||||
|
continueAtTsInPredecessorRoom === undefined
|
||||||
|
) {
|
||||||
|
throw new StatusError(
|
||||||
|
500,
|
||||||
|
`You navigated past the end of the room and it has a predecessor set (${predecessorRoomId}) ` +
|
||||||
|
`but we were unable to find a suitable place to jump to and continue from. ` +
|
||||||
|
`We could just redirect you to that predecessor room but we decided to throw an error ` +
|
||||||
|
`instead because we should be able to fallback to the room creation time in any case. ` +
|
||||||
|
`In other words, there shouldn't be a reason why we can't fetch the \`m.room.create\`` +
|
||||||
|
`event for this room unless the server is just broken right now. You can try refreshing to try again.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jump to the predecessor room at the appropriate timestamp to continue from.
|
||||||
|
// Since we're going backwards, we already know where to go so we can navigate
|
||||||
|
// straight there.
|
||||||
|
res.redirect(
|
||||||
|
matrixPublicArchiveURLCreator.archiveUrlForDate(
|
||||||
|
predecessorRoomId,
|
||||||
|
// XXX: We should probably go fetch and use the timestamp from
|
||||||
|
// `predecessorLastKnownEventId` here but that requires an extra
|
||||||
|
// `timestampToEvent(...)` lookup. We can assume it's close to the
|
||||||
|
// tombstone for now.
|
||||||
|
new Date(continueAtTsInPredecessorRoom),
|
||||||
|
{
|
||||||
|
viaServers: Array.from(predecessorViaServers || []),
|
||||||
|
scrollStartEventId: predecessorLastKnownEventId || tombstoneEventId,
|
||||||
|
// We can just visit a rough time where the tombstone is as we assume
|
||||||
|
// it's the last event in the room or at least the last event we care
|
||||||
|
// about. A given day should be good for most cases but it's possible
|
||||||
|
// that messages are sent after the tombstone and we end up missing the
|
||||||
|
// tombstone.
|
||||||
|
preferredPrecision: TIME_PRECISION_VALUES.none,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (dir === DIRECTION.forward) {
|
||||||
|
const { successorRoomId } = await fetchSuccessorInfo(matrixAccessToken, roomId);
|
||||||
|
if (successorRoomId) {
|
||||||
|
// Jump to the successor room and continue at the first event of the room
|
||||||
|
res.redirect(
|
||||||
|
matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(successorRoomId, {
|
||||||
|
dir: DIRECTION.forward,
|
||||||
|
currentRangeStartTs: 0,
|
||||||
|
currentRangeEndTs: 0,
|
||||||
|
// We don't need to define
|
||||||
|
// `currentRangeStartEventId`/`currentRangeEndEventId` here because we're
|
||||||
|
// jumping to a completely new room so the event IDs won't pertain to the
|
||||||
|
// new room and we don't have any to use anyway.
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Only throw if it's something other than a 404 error. 404 errors are fine, they
|
// Only throw if it's something other than a 404 error. 404 errors are fine, they
|
||||||
// just mean there is no more messages to paginate in that room and we were
|
// just mean there is no more messages to paginate in that room and we were
|
||||||
// already viewing the latest in the room.
|
// already viewing the latest in the room.
|
||||||
if (!is404Error) {
|
else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -511,6 +728,7 @@ router.get(
|
||||||
timeoutMiddleware,
|
timeoutMiddleware,
|
||||||
// eslint-disable-next-line max-statements, complexity
|
// eslint-disable-next-line max-statements, complexity
|
||||||
asyncHandler(async function (req, res) {
|
asyncHandler(async function (req, res) {
|
||||||
|
const nowTs = Date.now();
|
||||||
const roomIdOrAlias = getRoomIdOrAliasFromReq(req);
|
const roomIdOrAlias = getRoomIdOrAliasFromReq(req);
|
||||||
|
|
||||||
// We pull this fresh from the config for each request to ensure we have an
|
// We pull this fresh from the config for each request to ensure we have an
|
||||||
|
@ -532,8 +750,8 @@ router.get(
|
||||||
precisionFromUrl = TIME_PRECISION_VALUES.minutes;
|
precisionFromUrl = TIME_PRECISION_VALUES.minutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just 404 if anyone is trying to view the future, no need to waste resources on that
|
// Just 404 if anyone is trying to view the future, no need to waste resources on
|
||||||
const nowTs = Date.now();
|
// that
|
||||||
if (toTimestamp > roundUpTimestampToUtcDay(nowTs)) {
|
if (toTimestamp > roundUpTimestampToUtcDay(nowTs)) {
|
||||||
throw new StatusError(
|
throw new StatusError(
|
||||||
404,
|
404,
|
||||||
|
@ -545,11 +763,12 @@ router.get(
|
||||||
|
|
||||||
// We have to wait for the room join to happen first before we can fetch
|
// We have to wait for the room join to happen first before we can fetch
|
||||||
// any of the additional room info or messages.
|
// any of the additional room info or messages.
|
||||||
const roomId = await ensureRoomJoined(
|
//
|
||||||
matrixAccessToken,
|
// XXX: It would be better if we just tried fetching first and assume that we are
|
||||||
roomIdOrAlias,
|
// already joined and only join after we see a 403 Forbidden error (we should do
|
||||||
parseViaServersFromUserInput(req.query.via)
|
// this for all places we `ensureRoomJoined`)
|
||||||
);
|
const viaServers = parseViaServersFromUserInput(req.query.via);
|
||||||
|
const roomId = await ensureRoomJoined(matrixAccessToken, roomIdOrAlias, viaServers);
|
||||||
|
|
||||||
// Do these in parallel to avoid the extra time in sequential round-trips
|
// Do these in parallel to avoid the extra time in sequential round-trips
|
||||||
// (we want to display the archive page faster)
|
// (we want to display the archive page faster)
|
||||||
|
@ -577,16 +796,65 @@ router.get(
|
||||||
|
|
||||||
// Only `world_readable` or `shared` rooms that are `public` are viewable in the archive
|
// Only `world_readable` or `shared` rooms that are `public` are viewable in the archive
|
||||||
const allowedToViewRoom =
|
const allowedToViewRoom =
|
||||||
roomData?.historyVisibility === 'world_readable' ||
|
roomData.historyVisibility === 'world_readable' ||
|
||||||
(roomData?.historyVisibility === 'shared' && roomData?.joinRule === 'public');
|
(roomData.historyVisibility === 'shared' && roomData.joinRule === 'public');
|
||||||
|
|
||||||
if (!allowedToViewRoom) {
|
if (!allowedToViewRoom) {
|
||||||
throw new StatusError(
|
throw new StatusError(
|
||||||
403,
|
403,
|
||||||
`Only \`world_readable\` or \`shared\` rooms that are \`public\` can be viewed in the archive. ${roomData.id} has m.room.history_visiblity=${roomData?.historyVisibility} m.room.join_rules=${roomData?.joinRule}`
|
`Only \`world_readable\` or \`shared\` rooms that are \`public\` can be viewed in the archive. ${roomData.id} has m.room.history_visiblity=${roomData.historyVisibility} m.room.join_rules=${roomData.joinRule}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Since we're looking backwards from the given day, if we don't see any events,
|
||||||
|
// then we can assume that it's before the start of the room (it's the only way we
|
||||||
|
// would see no events).
|
||||||
|
const hasNavigatedBeforeStartOfRoom = events.length === 0;
|
||||||
|
// Check if we need to navigate backward to the predecessor room
|
||||||
|
if (hasNavigatedBeforeStartOfRoom && roomData.predecessorRoomId) {
|
||||||
|
// Jump to the predecessor room at the date/time the user is trying to visit at
|
||||||
|
res.redirect(
|
||||||
|
matrixPublicArchiveURLCreator.archiveUrlForDate(
|
||||||
|
roomData.predecessorRoomId,
|
||||||
|
new Date(toTimestamp),
|
||||||
|
{
|
||||||
|
preferredPrecision: precisionFromUrl,
|
||||||
|
// XXX: Should we also try combining `viaServers` we used to get to this room?
|
||||||
|
viaServers: Array.from(roomData.predecessorViaServers || []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care to navigate to the successor room if we're trying to view something
|
||||||
|
// past when the successor was set (it's an indicator that we need to go to the new
|
||||||
|
// room from this time forward).
|
||||||
|
const isNavigatedPastSuccessor = toTimestamp > roomData.successorSetTs;
|
||||||
|
// But if we're viewing the day when the successor was set, we want to allow viewing
|
||||||
|
// the room up until the successor was set.
|
||||||
|
const newestEvent = events[events.length - 1];
|
||||||
|
const isNewestEventFromSameDay =
|
||||||
|
newestEvent &&
|
||||||
|
newestEvent?.origin_server_ts &&
|
||||||
|
areTimestampsFromSameUtcDay(toTimestamp, newestEvent?.origin_server_ts);
|
||||||
|
// Check if we need to navigate forward to the successor room
|
||||||
|
if (roomData.successorRoomId && isNavigatedPastSuccessor && !isNewestEventFromSameDay) {
|
||||||
|
// Jump to the successor room at the date/time the user is trying to visit at
|
||||||
|
res.redirect(
|
||||||
|
matrixPublicArchiveURLCreator.archiveUrlForDate(
|
||||||
|
roomData.successorRoomId,
|
||||||
|
new Date(toTimestamp),
|
||||||
|
{
|
||||||
|
preferredPrecision: precisionFromUrl,
|
||||||
|
// Just try to pass on the `viaServers` the user was using to get to this room
|
||||||
|
viaServers: Array.from(viaServers || []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Default to no indexing (safe default)
|
// Default to no indexing (safe default)
|
||||||
let shouldIndex = false;
|
let shouldIndex = false;
|
||||||
if (stopSearchEngineIndexing) {
|
if (stopSearchEngineIndexing) {
|
||||||
|
@ -600,6 +868,7 @@ router.get(
|
||||||
const stylesUrl = urlJoin(basePath, '/css/styles.css');
|
const stylesUrl = urlJoin(basePath, '/css/styles.css');
|
||||||
const jsBundleUrl = urlJoin(basePath, '/js/entry-client-hydrogen.es.js');
|
const jsBundleUrl = urlJoin(basePath, '/js/entry-client-hydrogen.es.js');
|
||||||
|
|
||||||
|
// XXX: The `renderHydrogenVmRenderScriptToPageHtml` API surface is pretty awkward
|
||||||
const pageHtml = await renderHydrogenVmRenderScriptToPageHtml(
|
const pageHtml = await renderHydrogenVmRenderScriptToPageHtml(
|
||||||
path.resolve(__dirname, '../../shared/hydrogen-vm-render-script.js'),
|
path.resolve(__dirname, '../../shared/hydrogen-vm-render-script.js'),
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,9 +28,15 @@ const VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP = {
|
||||||
'!': 'roomid',
|
'!': 'roomid',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP = {
|
||||||
|
r: '#',
|
||||||
|
roomid: '!',
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
MS_LOOKUP,
|
MS_LOOKUP,
|
||||||
TIME_PRECISION_VALUES,
|
TIME_PRECISION_VALUES,
|
||||||
DIRECTION,
|
DIRECTION,
|
||||||
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
|
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
|
||||||
|
VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP,
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,6 +32,7 @@ function roundUpTimestampToUtcSecond(ts) {
|
||||||
return dateRountedUp.getTime();
|
return dateRountedUp.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Should these just be renamed to `roundDownTimestampToUtcDay`?
|
||||||
function getUtcStartOfDayTs(ts) {
|
function getUtcStartOfDayTs(ts) {
|
||||||
assert(typeof ts === 'number' || ts instanceof Date);
|
assert(typeof ts === 'number' || ts instanceof Date);
|
||||||
const date = new Date(ts);
|
const date = new Date(ts);
|
||||||
|
|
|
@ -58,6 +58,7 @@ class URLCreator {
|
||||||
|
|
||||||
archiveUrlForRoom(roomIdOrAlias, { viaServers = [] } = {}) {
|
archiveUrlForRoom(roomIdOrAlias, { viaServers = [] } = {}) {
|
||||||
assert(roomIdOrAlias);
|
assert(roomIdOrAlias);
|
||||||
|
assert(Array.isArray(viaServers));
|
||||||
let qs = new URLSearchParams();
|
let qs = new URLSearchParams();
|
||||||
[].concat(viaServers).forEach((viaServer) => {
|
[].concat(viaServers).forEach((viaServer) => {
|
||||||
qs.append('via', viaServer);
|
qs.append('via', viaServer);
|
||||||
|
@ -75,6 +76,7 @@ class URLCreator {
|
||||||
) {
|
) {
|
||||||
assert(roomIdOrAlias);
|
assert(roomIdOrAlias);
|
||||||
assert(date);
|
assert(date);
|
||||||
|
assert(Array.isArray(viaServers));
|
||||||
// `preferredPrecision` is optional but if they gave a value, make sure it's something expected
|
// `preferredPrecision` is optional but if they gave a value, make sure it's something expected
|
||||||
if (preferredPrecision) {
|
if (preferredPrecision) {
|
||||||
assert(
|
assert(
|
||||||
|
@ -117,16 +119,39 @@ class URLCreator {
|
||||||
)}${qsToUrlPiece(qs)}`;
|
)}${qsToUrlPiece(qs)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
archiveJumpUrlForRoom(roomIdOrAlias, { dir, currentRangeStartTs, currentRangeEndTs }) {
|
archiveJumpUrlForRoom(
|
||||||
|
roomIdOrAlias,
|
||||||
|
{
|
||||||
|
dir,
|
||||||
|
currentRangeStartTs,
|
||||||
|
currentRangeEndTs,
|
||||||
|
timelineStartEventId,
|
||||||
|
timelineEndEventId,
|
||||||
|
viaServers = [],
|
||||||
|
}
|
||||||
|
) {
|
||||||
assert(roomIdOrAlias);
|
assert(roomIdOrAlias);
|
||||||
assert(dir);
|
assert(dir);
|
||||||
assert(currentRangeStartTs);
|
assert(typeof currentRangeStartTs === 'number');
|
||||||
assert(currentRangeEndTs);
|
assert(typeof currentRangeEndTs === 'number');
|
||||||
|
assert(Array.isArray(viaServers));
|
||||||
|
// `timelineStartEventId` and `timelineEndEventId` are optional because the
|
||||||
|
// timeline could be showing 0 events or we could be jumping with no knowledge of
|
||||||
|
// what was shown before.
|
||||||
|
|
||||||
let qs = new URLSearchParams();
|
let qs = new URLSearchParams();
|
||||||
qs.append('dir', dir);
|
qs.append('dir', dir);
|
||||||
qs.append('currentRangeStartTs', currentRangeStartTs);
|
qs.append('currentRangeStartTs', currentRangeStartTs);
|
||||||
qs.append('currentRangeEndTs', currentRangeEndTs);
|
qs.append('currentRangeEndTs', currentRangeEndTs);
|
||||||
|
if (timelineStartEventId) {
|
||||||
|
qs.append('timelineStartEventId', timelineStartEventId);
|
||||||
|
}
|
||||||
|
if (timelineEndEventId) {
|
||||||
|
qs.append('timelineEndEventId', timelineEndEventId);
|
||||||
|
}
|
||||||
|
[].concat(viaServers).forEach((viaServer) => {
|
||||||
|
qs.append('via', viaServer);
|
||||||
|
});
|
||||||
|
|
||||||
const urlPath = this._getArchiveUrlPathForRoomIdOrAlias(roomIdOrAlias);
|
const urlPath = this._getArchiveUrlPathForRoomIdOrAlias(roomIdOrAlias);
|
||||||
|
|
||||||
|
|
|
@ -363,6 +363,9 @@ class ArchiveRoomViewModel extends ViewModel {
|
||||||
// to paginate from
|
// to paginate from
|
||||||
const jumpRangeEndTimestamp = this._dayTimestampTo;
|
const jumpRangeEndTimestamp = this._dayTimestampTo;
|
||||||
|
|
||||||
|
const timelineStartEventId = events[0]?.event_id;
|
||||||
|
const timelineEndEventId = events[events.length - 1]?.event_id;
|
||||||
|
|
||||||
// Check whether the given day represented in the URL has any events on the page
|
// Check whether the given day represented in the URL has any events on the page
|
||||||
// from that day. We only need to check the last event which would be closest to
|
// from that day. We only need to check the last event which would be closest to
|
||||||
// `_dayTimestampTo` anyway.
|
// `_dayTimestampTo` anyway.
|
||||||
|
@ -394,6 +397,8 @@ class ArchiveRoomViewModel extends ViewModel {
|
||||||
canonicalAlias: this._room.canonicalAlias,
|
canonicalAlias: this._room.canonicalAlias,
|
||||||
jumpRangeStartTimestamp,
|
jumpRangeStartTimestamp,
|
||||||
jumpRangeEndTimestamp,
|
jumpRangeEndTimestamp,
|
||||||
|
timelineStartEventId,
|
||||||
|
timelineEndEventId,
|
||||||
// This is a bit cheating but I don't know how else to pass this kind of
|
// This is a bit cheating but I don't know how else to pass this kind of
|
||||||
// info to the Tile viewmodel
|
// info to the Tile viewmodel
|
||||||
basePath: this._basePath,
|
basePath: this._basePath,
|
||||||
|
@ -417,6 +422,8 @@ class ArchiveRoomViewModel extends ViewModel {
|
||||||
dayTimestamp: this._dayTimestampTo,
|
dayTimestamp: this._dayTimestampTo,
|
||||||
jumpRangeStartTimestamp,
|
jumpRangeStartTimestamp,
|
||||||
jumpRangeEndTimestamp,
|
jumpRangeEndTimestamp,
|
||||||
|
timelineStartEventId,
|
||||||
|
timelineEndEventId,
|
||||||
// This is a bit cheating but I don't know how else to pass this kind of
|
// This is a bit cheating but I don't know how else to pass this kind of
|
||||||
// info to the Tile viewmodel
|
// info to the Tile viewmodel
|
||||||
basePath: this._basePath,
|
basePath: this._basePath,
|
||||||
|
|
|
@ -38,6 +38,16 @@ class JumpToNextActivitySummaryTileViewModel extends SimpleTile {
|
||||||
return this._entry?.content?.['jumpRangeEndTimestamp'];
|
return this._entry?.content?.['jumpRangeEndTimestamp'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The first event shown in the timeline.
|
||||||
|
get timelineStartEventId() {
|
||||||
|
return this._entry?.content?.['timelineStartEventId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last event shown in the timeline.
|
||||||
|
get timelineEndEventId() {
|
||||||
|
return this._entry?.content?.['timelineEndEventId'];
|
||||||
|
}
|
||||||
|
|
||||||
get jumpToNextActivityUrl() {
|
get jumpToNextActivityUrl() {
|
||||||
return this._matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(
|
return this._matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(
|
||||||
this._entry?.content?.['canonicalAlias'] || this._entry.roomId,
|
this._entry?.content?.['canonicalAlias'] || this._entry.roomId,
|
||||||
|
@ -45,6 +55,8 @@ class JumpToNextActivitySummaryTileViewModel extends SimpleTile {
|
||||||
dir: DIRECTION.forward,
|
dir: DIRECTION.forward,
|
||||||
currentRangeStartTs: this.jumpRangeStartTimestamp,
|
currentRangeStartTs: this.jumpRangeStartTimestamp,
|
||||||
currentRangeEndTs: this.jumpRangeEndTimestamp,
|
currentRangeEndTs: this.jumpRangeEndTimestamp,
|
||||||
|
timelineStartEventId: this.timelineStartEventId,
|
||||||
|
timelineEndEventId: this.timelineEndEventId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,16 @@ class JumpToPreviousActivitySummaryTileViewModel extends SimpleTile {
|
||||||
return this._entry?.content?.['jumpRangeEndTimestamp'];
|
return this._entry?.content?.['jumpRangeEndTimestamp'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The first event shown in the timeline.
|
||||||
|
get timelineStartEventId() {
|
||||||
|
return this._entry?.content?.['timelineStartEventId'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last event shown in the timeline.
|
||||||
|
get timelineEndEventId() {
|
||||||
|
return this._entry?.content?.['timelineEndEventId'];
|
||||||
|
}
|
||||||
|
|
||||||
get jumpToPreviousActivityUrl() {
|
get jumpToPreviousActivityUrl() {
|
||||||
return this._matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(
|
return this._matrixPublicArchiveURLCreator.archiveJumpUrlForRoom(
|
||||||
this._entry?.content?.['canonicalAlias'] || this._entry.roomId,
|
this._entry?.content?.['canonicalAlias'] || this._entry.roomId,
|
||||||
|
@ -37,6 +47,8 @@ class JumpToPreviousActivitySummaryTileViewModel extends SimpleTile {
|
||||||
dir: DIRECTION.backward,
|
dir: DIRECTION.backward,
|
||||||
currentRangeStartTs: this.jumpRangeStartTimestamp,
|
currentRangeStartTs: this.jumpRangeStartTimestamp,
|
||||||
currentRangeEndTs: this.jumpRangeEndTimestamp,
|
currentRangeEndTs: this.jumpRangeEndTimestamp,
|
||||||
|
timelineStartEventId: this.timelineStartEventId,
|
||||||
|
timelineEndEventId: this.timelineEndEventId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
1807
test/e2e-tests.js
1807
test/e2e-tests.js
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,7 @@
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const urlJoin = require('url-join');
|
const urlJoin = require('url-join');
|
||||||
const { fetchEndpointAsJson, fetchEndpoint } = require('../../server/lib/fetch-endpoint');
|
const { fetchEndpointAsJson, fetchEndpoint } = require('../../server/lib/fetch-endpoint');
|
||||||
|
const getServerNameFromMatrixRoomIdOrAlias = require('../../server/lib/matrix-utils/get-server-name-from-matrix-room-id-or-alias');
|
||||||
|
|
||||||
const config = require('../../server/lib/config');
|
const config = require('../../server/lib/config');
|
||||||
const matrixAccessToken = config.get('matrixAccessToken');
|
const matrixAccessToken = config.get('matrixAccessToken');
|
||||||
|
@ -85,6 +86,62 @@ async function getTestClientForHs(testMatrixServerUrl) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Create a public room to test in
|
// Create a public room to test in
|
||||||
async function createTestRoom(client, overrideCreateOptions = {}) {
|
async function createTestRoom(client, overrideCreateOptions = {}) {
|
||||||
let qs = new URLSearchParams();
|
let qs = new URLSearchParams();
|
||||||
|
@ -103,15 +160,7 @@ async function createTestRoom(client, overrideCreateOptions = {}) {
|
||||||
preset: 'public_chat',
|
preset: 'public_chat',
|
||||||
name: roomName,
|
name: roomName,
|
||||||
room_alias_name: roomAlias,
|
room_alias_name: roomAlias,
|
||||||
initial_state: [
|
initial_state: [WORLD_READABLE_STATE_EVENT],
|
||||||
{
|
|
||||||
type: 'm.room.history_visibility',
|
|
||||||
state_key: '',
|
|
||||||
content: {
|
|
||||||
history_visibility: 'world_readable',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
...overrideCreateOptions,
|
...overrideCreateOptions,
|
||||||
},
|
},
|
||||||
|
@ -124,6 +173,64 @@ async function createTestRoom(client, overrideCreateOptions = {}) {
|
||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getCanonicalAlias({ client, roomId }) {
|
async function getCanonicalAlias({ client, roomId }) {
|
||||||
const { data: stateCanonicalAliasRes } = await fetchEndpointAsJson(
|
const { data: stateCanonicalAliasRes } = await fetchEndpointAsJson(
|
||||||
urlJoin(
|
urlJoin(
|
||||||
|
@ -167,54 +274,6 @@ async function joinRoom({ client, roomId, viaServers }) {
|
||||||
return joinedRoomId;
|
return joinedRoomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (stateKey) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendMessage({ client, roomId, content, timestamp }) {
|
async function sendMessage({ client, roomId, content, timestamp }) {
|
||||||
return sendEvent({ client, roomId, eventType: 'm.room.message', content, timestamp });
|
return sendEvent({ client, roomId, eventType: 'm.room.message', content, timestamp });
|
||||||
}
|
}
|
||||||
|
@ -259,6 +318,28 @@ async function createMessagesInRoom({
|
||||||
return { eventIds, eventMap };
|
return { eventIds, eventMap };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
async function updateProfile({ client, displayName, avatarUrl }) {
|
async function updateProfile({ client, displayName, avatarUrl }) {
|
||||||
let qs = new URLSearchParams();
|
let qs = new URLSearchParams();
|
||||||
if (client.applicationServiceUserIdOverride) {
|
if (client.applicationServiceUserIdOverride) {
|
||||||
|
@ -345,11 +426,13 @@ module.exports = {
|
||||||
getTestClientForAs,
|
getTestClientForAs,
|
||||||
getTestClientForHs,
|
getTestClientForHs,
|
||||||
createTestRoom,
|
createTestRoom,
|
||||||
|
upgradeTestRoom,
|
||||||
getCanonicalAlias,
|
getCanonicalAlias,
|
||||||
joinRoom,
|
joinRoom,
|
||||||
sendEvent,
|
sendEvent,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
createMessagesInRoom,
|
createMessagesInRoom,
|
||||||
|
getMessagesInRoom,
|
||||||
updateProfile,
|
updateProfile,
|
||||||
uploadContent,
|
uploadContent,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP,
|
VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP,
|
||||||
} = require('matrix-public-archive-shared/lib/reference-values');
|
} = require('matrix-public-archive-shared/lib/reference-values');
|
||||||
|
|
||||||
// http://archive.matrix.org/r/some-room:matrix.org/date/2022/11/16T23:59:59?at=$xxx
|
// http://archive.matrix.org/r/some-room:matrix.org/date/2022/11/16T23:59:59?at=$xxx
|
||||||
|
@ -13,9 +13,7 @@ function parseArchiveUrlForRoom(archiveUrlForRoom) {
|
||||||
/\/(r|roomid)\/(.*?)\/date\/(.*)/
|
/\/(r|roomid)\/(.*?)\/date\/(.*)/
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sigil] = Object.entries(VALID_SIGIL_TO_ENTITY_DESCRIPTOR_MAP).find(
|
const sigil = VALID_ENTITY_DESCRIPTOR_TO_SIGIL_MAP[roomIdOrAliasDescriptor];
|
||||||
([_sigil, entityDescriptor]) => roomIdOrAliasDescriptor === entityDescriptor
|
|
||||||
);
|
|
||||||
const roomIdOrAlias = `${sigil}${roomIdOrAliasUrlPart}`;
|
const roomIdOrAlias = `${sigil}${roomIdOrAliasUrlPart}`;
|
||||||
|
|
||||||
const continueAtEvent = urlObj.searchParams.get('at');
|
const continueAtEvent = urlObj.searchParams.get('at');
|
||||||
|
|
|
@ -191,19 +191,9 @@ function parseRoomDayMessageStructure(roomDayMessageStructureString) {
|
||||||
events,
|
events,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
// Ensure that each page has the same number of events on it
|
|
||||||
const numEventsOnEachPage = pages.map((page) => page.events.length);
|
|
||||||
// The page limit is X but each page will display X + 1 messages because we fetch one
|
|
||||||
// extra to determine overflow.
|
|
||||||
const archiveMessageLimit = numEventsOnEachPage[0] - 1;
|
|
||||||
assert(
|
|
||||||
numEventsOnEachPage.every((numEvents) => numEvents === archiveMessageLimit + 1),
|
|
||||||
`Expected all pages to have the same number of events (archiveMessageLimit + 1) where archiveMessageLimit=${archiveMessageLimit} but found ${numEventsOnEachPage}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rooms,
|
rooms,
|
||||||
archiveMessageLimit,
|
|
||||||
pages,
|
pages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue