1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-26 17:03:12 +03:00

Merge branch 'develop' into loglevel-extend

This commit is contained in:
Janith Kasun
2019-05-19 09:40:38 +05:30
committed by GitHub
48 changed files with 1281 additions and 434 deletions

View File

@@ -1,6 +1,8 @@
{
"presets": ["es2015"],
"plugins": [
"transform-class-properties",
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).

View File

@@ -22,3 +22,13 @@ steps:
plugins:
- docker#v3.0.1:
image: "node:10"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true

View File

@@ -14,6 +14,9 @@ module.exports = {
es6: true,
},
extends: ["eslint:recommended", "google"],
plugins: [
"babel",
],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
@@ -73,5 +76,10 @@ module.exports = {
"asyncArrow": "always",
}],
"arrow-parens": "off",
// eslint's built in no-invalid-this rule breaks with class properties
"no-invalid-this": "off",
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
}
}

View File

@@ -1,3 +1,65 @@
Changes in [1.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0) (2019-05-07)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0-rc.1...v1.1.0)
* No Changes since rc.1
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.4...v1.1.0-rc.1)
* use the release version of olm 3.1.0
[\#903](https://github.com/matrix-org/matrix-js-sdk/pull/903)
* Use new Olm repo link in README
[\#901](https://github.com/matrix-org/matrix-js-sdk/pull/901)
* Support being fed a .well-known config object for validation
[\#897](https://github.com/matrix-org/matrix-js-sdk/pull/897)
* emit self-membership event at end of handling sync update
[\#900](https://github.com/matrix-org/matrix-js-sdk/pull/900)
* Use packages.matrix.org for Olm
[\#898](https://github.com/matrix-org/matrix-js-sdk/pull/898)
* Fix tests on develop
[\#899](https://github.com/matrix-org/matrix-js-sdk/pull/899)
* Stop syncing when the token is invalid
[\#895](https://github.com/matrix-org/matrix-js-sdk/pull/895)
* change event redact, POST request to PUT request
[\#887](https://github.com/matrix-org/matrix-js-sdk/pull/887)
* Expose better autodiscovery error messages
[\#894](https://github.com/matrix-org/matrix-js-sdk/pull/894)
* Explicitly guard store usage during sync startup
[\#892](https://github.com/matrix-org/matrix-js-sdk/pull/892)
* Flag v3 rooms as safe
[\#893](https://github.com/matrix-org/matrix-js-sdk/pull/893)
* Cache failed capabilities lookups for shorter amounts of time
[\#890](https://github.com/matrix-org/matrix-js-sdk/pull/890)
* Fix highlight notifications for unencrypted rooms
[\#891](https://github.com/matrix-org/matrix-js-sdk/pull/891)
* Document checking crypto state before using `hasUnverifiedDevices`
[\#889](https://github.com/matrix-org/matrix-js-sdk/pull/889)
* Add logging to sync startup path
[\#888](https://github.com/matrix-org/matrix-js-sdk/pull/888)
* Track e2e highlights better, particularly in 'Mentions Only' rooms
[\#886](https://github.com/matrix-org/matrix-js-sdk/pull/886)
* support both the incorrect and correct MAC methods
[\#882](https://github.com/matrix-org/matrix-js-sdk/pull/882)
* Refuse to set forwards pagination token on live timeline
[\#885](https://github.com/matrix-org/matrix-js-sdk/pull/885)
* Degrade `IndexedDBStore` back to memory only on failure
[\#884](https://github.com/matrix-org/matrix-js-sdk/pull/884)
* Refuse to link live timelines into the forwards/backwards position when
either is invalid
[\#877](https://github.com/matrix-org/matrix-js-sdk/pull/877)
* Key backup logging improvements
[\#883](https://github.com/matrix-org/matrix-js-sdk/pull/883)
* Don't assume aborts are always from txn.abort()
[\#880](https://github.com/matrix-org/matrix-js-sdk/pull/880)
* Add a bunch of logging
[\#878](https://github.com/matrix-org/matrix-js-sdk/pull/878)
* Refuse splicing the live timeline into a broken position
[\#873](https://github.com/matrix-org/matrix-js-sdk/pull/873)
* Add existence check to local storage based crypto store
[\#872](https://github.com/matrix-org/matrix-js-sdk/pull/872)
Changes in [1.0.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4) (2019-04-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3...v1.0.4)

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "1.0.4",
"version": "1.1.0",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -9,7 +9,7 @@
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "yarn start:init && yarn start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
@@ -54,7 +54,6 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"base-x": "3.0.4",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
@@ -68,12 +67,14 @@
"babel-cli": "^6.18.0",
"babel-eslint": "^10.0.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
@@ -93,5 +94,8 @@
"transform": [
"sourceify"
]
},
"resolutions": {
"bs58/base-x": "3.0.4"
}
}

View File

@@ -88,7 +88,7 @@ describe("DeviceList management:", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.

View File

@@ -406,7 +406,7 @@ describe("MatrixClient crypto", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();

View File

@@ -15,7 +15,7 @@ describe("MatrixClient events", function() {
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({

View File

@@ -103,7 +103,7 @@ describe("getEventTimeline support", function() {
let client;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -228,7 +228,7 @@ describe("MatrixClient event timelines", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);

View File

@@ -21,7 +21,7 @@ describe("MatrixClient", function() {
const accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
store = new MemoryStore();

View File

@@ -58,7 +58,7 @@ describe("MatrixClient opts", function() {
};
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
});

View File

@@ -20,7 +20,7 @@ describe("MatrixClient retrying", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();

View File

@@ -104,7 +104,7 @@ describe("MatrixClient room timelines", function() {
}
beforeEach(function(done) {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({

View File

@@ -23,7 +23,7 @@ describe("MatrixClient syncing", function() {
const roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({

View File

@@ -283,7 +283,7 @@ describe("megolm", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",

View File

@@ -30,7 +30,7 @@ describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});

View File

@@ -9,7 +9,7 @@ describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("getHttpUriForMxc", function() {

View File

@@ -60,7 +60,7 @@ describe('DeviceList', function() {
let deviceLists = [];
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
deviceLists = [];

View File

@@ -32,7 +32,7 @@ describe("MegolmDecryption", function() {
let mockBaseApis;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await Olm.init();

View File

@@ -54,7 +54,7 @@ describe("OlmDecryption", function() {
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await global.Olm.init();

View File

@@ -126,7 +126,7 @@ describe("MegolmBackup", function() {
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();

View File

@@ -29,7 +29,7 @@ export async function makeTestClients(userInfos, options) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line no-invalid-this
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: msg,
});

View File

@@ -18,7 +18,7 @@ describe("EventTimeline", function() {
let timeline;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId }};

View File

@@ -25,7 +25,7 @@ import logger from '../../src/logger';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe(".attemptDecryption", () => {

View File

@@ -12,7 +12,7 @@ describe("Filter", function() {
let filter;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
filter = new Filter(userId);
});

View File

@@ -36,7 +36,7 @@ class FakeClient {
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
it("should start an auth stage and complete it", function(done) {

View File

@@ -125,7 +125,7 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",

View File

@@ -15,7 +15,7 @@ describe("realtime-callbacks", function() {
}
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
@@ -56,8 +56,8 @@ describe("realtime-callbacks", function() {
it("should set 'this' to the global object", function() {
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);

View File

@@ -14,7 +14,7 @@ describe("RoomMember", function() {
let member;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
member = new RoomMember(roomId, userA);
});

View File

@@ -17,7 +17,7 @@ describe("RoomState", function() {
let state;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined

View File

@@ -19,7 +19,7 @@ describe("Room", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =

View File

@@ -26,7 +26,7 @@ describe("MatrixScheduler", function() {
});
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {

View File

@@ -26,7 +26,7 @@ describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});

View File

@@ -68,7 +68,7 @@ function createLinkedTimelines() {
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("minIndex", function() {
@@ -164,7 +164,7 @@ describe("TimelineWindow", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("load", function() {

View File

@@ -11,7 +11,7 @@ describe("User", function() {
let user;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
user = new User(userId);
});

View File

@@ -7,7 +7,7 @@ import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("encodeParams", function() {

View File

@@ -480,7 +480,7 @@ export class AutoDiscovery {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url },
{ method: "GET", uri: url, timeout: 5000 },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
let action = "FAIL_PROMPT";

View File

@@ -904,7 +904,7 @@ MatrixBaseApis.prototype.redactEvent = function(
callback = txnId;
}
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId/$tnxId", {
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId/$txnId", {
$roomId: roomId,
$eventId: eventId,
$txnId: txnId ? txnId : this.makeTxnId(),

View File

@@ -149,6 +149,11 @@ function keyFromRecoverySession(session, decryptionKey) {
* maintain support for back-paginating the live timeline after a '/sync'
* result with a gap.
*
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @param {Array} [opts.verificationMethods] Optional. The verification method
* that the application can handle. Each element should be an item from {@link
* module:crypto~verificationMethods verificationMethods}, or a class that
@@ -214,6 +219,7 @@ function MatrixClient(opts) {
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
this._notifTimelineSet = null;
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
this._crypto = null;
this._cryptoStore = opts.cryptoStore;
@@ -1712,7 +1718,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
content: content,
});
localEvent._txnId = txnId;
localEvent.status = EventStatus.SENDING;
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
if (room) {
@@ -1842,7 +1848,7 @@ function _updatePendingEventStatus(room, event, newStatus) {
if (room) {
room.updatePendingEvent(event, newStatus);
} else {
event.status = newStatus;
event.setStatus(newStatus);
}
}
@@ -4157,6 +4163,10 @@ function _PojoToMatrixEventMapper(client) {
]);
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
}
return mapper;

View File

@@ -378,7 +378,11 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
);
sigInfo.valid = true;
} catch (e) {
logger.info("Bad signature from key ID " + keyId, e);
logger.info(
"Bad signature from key ID " + keyId + " userID " + this._userId +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {

View File

@@ -21,6 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
import logger from '../../src/logger';
import Relations from './relations';
// var DEBUG = false;
const DEBUG = true;
@@ -55,22 +56,38 @@ if (DEBUG) {
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
* @param {?Room} room
* Room for this timelineSet. May be null for non-room cases, such as the
* notification timeline.
* @param {Object} opts Options inherited from Room.
*
* @param {boolean} [opts.timelineSupport = false]
* Set to true to enable improved timeline support.
* @param {Object} [opts.filter = null]
* The filter object, if any, for this timelineSet.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
if (this._unstableClientRelationAggregation) {
// A tree of objects to access a set of relations for an event, as in:
// this._relations[relatesToEventId][relationType][relationEventType]
this._relations = {};
}
}
utils.inherits(EventTimelineSet, EventEmitter);
@@ -524,6 +541,9 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
@@ -658,6 +678,122 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
return null;
};
/**
* Get a collection of relations to a given event in this timeline set.
*
* @param {String} eventId
* The ID of the event that you'd like to access relation events for.
* For example, with annotations, this would be the ID of the event being annotated.
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
*
* @returns {Relations}
* A container for relation events.
*/
EventTimelineSet.prototype.getRelationsForEvent = function(
eventId, relationType, eventType,
) {
if (!this._unstableClientRelationAggregation) {
throw new Error("Client-side relation aggregation is disabled");
}
if (!eventId || !relationType || !eventType) {
throw new Error("Invalid arguments for `getRelationsForEvent`");
}
// debuglog("Getting relations for: ", eventId, relationType, eventType);
const relationsForEvent = this._relations[eventId] || {};
const relationsWithRelType = relationsForEvent[relationType] || {};
return relationsWithRelType[eventType];
};
/**
* Set an event as the target event if any Relations exist for it already
*
* @param {MatrixEvent} event
* The event to check as relation target.
*/
EventTimelineSet.prototype.setRelationsTarget = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
const relationsForEvent = this._relations[event.getId()];
if (!relationsForEvent) {
return;
}
// don't need it for non m.replace relations for now
const relationsWithRelType = relationsForEvent["m.replace"];
if (!relationsWithRelType) {
return;
}
// only doing replacements for messages for now (e.g. edits)
const relationsWithEventType = relationsWithRelType["m.room.message"];
if (relationsWithEventType) {
relationsWithEventType.setTargetEvent(event);
}
};
/**
* Add relation events to the relevant relation collection.
*
* @param {MatrixEvent} event
* The new relation event to be aggregated.
*/
EventTimelineSet.prototype.aggregateRelations = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted()) {
event.once("Event.decrypted", () => {
this.aggregateRelations(event);
});
return;
}
const relation = event.getRelation();
if (!relation) {
return;
}
const relatesToEventId = relation.event_id;
const relationType = relation.rel_type;
const eventType = event.getType();
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
let relationsForEvent = this._relations[relatesToEventId];
if (!relationsForEvent) {
relationsForEvent = this._relations[relatesToEventId] = {};
}
let relationsWithRelType = relationsForEvent[relationType];
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
}
let relationsWithEventType = relationsWithRelType[eventType];
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
const relatesToEvent = this.findEventById(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
}
}
relationsWithEventType.addEvent(event);
};
/**
* The EventTimelineSet class.
*/

View File

@@ -51,6 +51,12 @@ module.exports.EventStatus = {
};
const interns = {};
function intern(str) {
if (!interns[str]) {
interns[str] = str;
}
return interns[str];
}
/**
* Construct a Matrix Event object
@@ -88,20 +94,25 @@ module.exports.MatrixEvent = function MatrixEvent(
if (!event[prop]) {
return;
}
if (!interns[event[prop]]) {
interns[event[prop]] = event[prop];
}
event[prop] = interns[event[prop]];
event[prop] = intern(event[prop]);
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (!event.content || !event.content[prop]) {
return;
}
if (!interns[event.content[prop]]) {
interns[event.content[prop]] = event.content[prop];
event.content[prop] = intern(event.content[prop]);
});
["rel_type"].forEach((prop) => {
if (
!event.content ||
!event.content["m.relates_to"] ||
!event.content["m.relates_to"][prop]
) {
return;
}
event.content[prop] = interns[event.content[prop]];
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
});
this.event = event || {};
@@ -112,6 +123,7 @@ module.exports.MatrixEvent = function MatrixEvent(
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._replacingEvent = null;
this._clearEvent = {};
@@ -210,12 +222,28 @@ utils.extend(module.exports.MatrixEvent.prototype, {
},
/**
* Get the (decrypted, if necessary) event content JSON.
* Get the (decrypted, if necessary) event content JSON, even if the event
* was replaced by another event.
*
* @return {Object} The event content JSON, or an empty object.
*/
getOriginalContent: function() {
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the (decrypted, if necessary) event content JSON,
* or the content from the replacing event, if any.
* See `makeReplaced`.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this._clearEvent.content || this.event.content || {};
if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {};
} else {
return this.getOriginalContent();
}
},
/**
@@ -651,6 +679,9 @@ utils.extend(module.exports.MatrixEvent.prototype, {
throw new Error("invalid redaction_event in makeRedacted");
}
this.emit("Event.beforeRedaction", this, redaction_event);
this._replacingEvent = null;
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
@@ -718,7 +749,131 @@ utils.extend(module.exports.MatrixEvent.prototype, {
handleRemoteEcho: function(event) {
this.event = event;
// successfully sent.
this.status = null;
this.setStatus(null);
},
/**
* Whether the event is in any phase of sending, send failure, waiting for
* remote echo, etc.
*
* @return {boolean}
*/
isSending() {
return !!this.status;
},
/**
* Update the event's sending status and emit an event as well.
*
* @param {String} status The new status
*/
setStatus(status) {
this.status = status;
this.emit("Event.status", this, status);
},
/**
* Get whether the event is a relation event, and of a given type if
* `relType` is passed in.
*
* @param {string?} relType if given, checks that the relation is of the
* given type
* @return {boolean}
*/
isRelation(relType = undefined) {
// Relation info is lifted out of the encrypted content when sent to
// encrypted rooms, so we have to check `getWireContent` for this.
const content = this.getWireContent();
const relation = content && content["m.relates_to"];
return relation && relation.rel_type && relation.event_id &&
((relType && relation.rel_type === relType) || !relType);
},
/**
* Get relation info for the event, if any.
*
* @return {Object}
*/
getRelation() {
if (!this.isRelation()) {
return null;
}
return this.getWireContent()["m.relates_to"];
},
/**
* Set an event that replaces the content of this event, through an m.replace relation.
*
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
*/
makeReplaced(newEvent) {
if (this.isRedacted()) {
return;
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
}
},
/**
* Returns the status of the event, or the replacing event in case `makeReplace` has been called.
*
* @return {EventStatus}
*/
replacementOrOwnStatus() {
if (this._replacingEvent) {
return this._replacingEvent.status;
} else {
return this.status;
}
},
/**
* Returns the event ID of the event replacing the content of this event, if any.
*
* @return {string?}
*/
replacingEventId() {
return this._replacingEvent && this._replacingEvent.getId();
},
/**
* Returns the event replacing the content of this event, if any.
*
* @return {MatrixEvent?}
*/
replacingEvent() {
return this._replacingEvent;
},
/**
* Summarise the event as JSON for debugging. If encrypted, include both the
* decrypted and encrypted view of the event. This is named `toJSON` for use
* with `JSON.stringify` which checks objects for functions named `toJSON`
* and will call them to customise the output if they are defined.
*
* @return {Object}
*/
toJSON() {
const event = {
type: this.getType(),
sender: this.getSender(),
content: this.getContent(),
event_id: this.getId(),
origin_server_ts: this.getTs(),
unsigned: this.getUnsigned(),
room_id: this.getRoomId(),
};
if (!this.isEncrypted()) {
return event;
}
return {
decrypted: event,
encrypted: this.event,
};
},
});

337
src/models/relations.js Normal file
View File

@@ -0,0 +1,337 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { EventStatus } from '../../lib/models/event';
/**
* A container for relation events that supports easy access to common ways of
* aggregating such events. Each instance holds events that of a single relation
* type and event type. All of the events also relate to the same original event.
*
* The typical way to get one of these containers is via
* EventTimelineSet#getRelationsForEvent.
*/
export default class Relations extends EventEmitter {
/**
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference",
* "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @param {?Room} room
* Room for this container. May be null for non-room cases, such as the
* notification timeline.
*/
constructor(relationType, eventType, room) {
super();
this.relationType = relationType;
this.eventType = eventType;
this._relations = new Set();
this._annotationsByKey = {};
this._annotationsBySender = {};
this._sortedAnnotationsByKey = [];
this._targetEvent = null;
}
/**
* Add relation events to this collection.
*
* @param {MatrixEvent} event
* The new relation event to be added.
*/
addEvent(event) {
if (this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
// If the event is in the process of being sent, listen for cancellation
// so we can remove the event from the collection.
if (event.isSending()) {
event.on("Event.status", this._onEventStatus);
}
this._relations.add(event);
if (this.relationType === "m.annotation") {
this._addAnnotationToAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
event.on("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.add", event);
}
/**
* Remove relation event from this collection.
*
* @param {MatrixEvent} event
* The relation event to remove.
*/
_removeEvent(event) {
if (!this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
this._relations.delete(event);
if (this.relationType === "m.annotation") {
this._removeAnnotationFromAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
this.emit("Relations.remove", event);
}
/**
* Listens for event status changes to remove cancelled events.
*
* @param {MatrixEvent} event The event whose status has changed
* @param {EventStatus} status The new status
*/
_onEventStatus = (event, status) => {
if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore
event.removeListener("Event.status", this._onEventStatus);
return;
}
if (status !== EventStatus.CANCELLED) {
return;
}
// Event was cancelled, remove from the collection
event.removeListener("Event.status", this._onEventStatus);
this._removeEvent(event);
}
/**
* Get all relation events in this collection.
*
* These are currently in the order of insertion to this collection, which
* won't match timeline order in the case of scrollback.
* TODO: Tweak `addEvent` to insert correctly for scrollback.
*
* @return {Array}
* Relation events in insertion order.
*/
getRelations() {
return [...this._relations];
}
_addAnnotationToAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
let eventsForKey = this._annotationsByKey[key];
if (!eventsForKey) {
eventsForKey = this._annotationsByKey[key] = new Set();
this._sortedAnnotationsByKey.push([key, eventsForKey]);
}
// Add the new event to the set for this key
eventsForKey.add(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
const sender = event.getSender();
let eventsFromSender = this._annotationsBySender[sender];
if (!eventsFromSender) {
eventsFromSender = this._annotationsBySender[sender] = new Set();
}
// Add the new event to the set for this sender
eventsFromSender.add(event);
}
_removeAnnotationFromAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
const eventsForKey = this._annotationsByKey[key];
if (eventsForKey) {
eventsForKey.delete(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
}
const sender = event.getSender();
const eventsFromSender = this._annotationsBySender[sender];
if (eventsFromSender) {
eventsFromSender.delete(event);
}
}
/**
* For relations that have been redacted, we want to remove them from
* aggregation data sets and emit an update event.
*
* To do so, we listen for `Event.beforeRedaction`, which happens:
* - after the server accepted the redaction and remote echoed back to us
* - before the original event has been marked redacted in the client
*
* @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted.
*/
_onBeforeRedaction = (redactedEvent) => {
if (!this._relations.has(redactedEvent)) {
return;
}
this._relations.delete(redactedEvent);
if (this.relationType === "m.annotation") {
// Remove the redacted annotation from aggregation by key
this._removeAnnotationFromAggregation(redactedEvent);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
// Dispatch a redaction event on this collection. `setTimeout` is used
// to wait until the next event loop iteration by which time the event
// has actually been marked as redacted.
setTimeout(() => {
this.emit("Relations.redaction");
}, 0);
}
/**
* Get all events in this collection grouped by key and sorted by descending
* event count in each group.
*
* This is currently only supported for the annotation relation type.
*
* @return {Array}
* An array of [key, events] pairs sorted by descending event count.
* The events are stored in a Set (which preserves insertion order).
*/
getSortedAnnotationsByKey() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._sortedAnnotationsByKey;
}
/**
* Get all events in this collection grouped by sender.
*
* This is currently only supported for the annotation relation type.
*
* @return {Object}
* An object with each relation sender as a key and the matching Set of
* events for that sender as a value.
*/
getAnnotationsBySender() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._annotationsBySender;
}
/**
* Returns the most recent (and allowed) m.replace relation, if any.
*
* This is currently only supported for the m.replace relation type,
* once the target event is known, see `addEvent`.
*
* @return {MatrixEvent?}
*/
getLastReplacement() {
if (this.relationType !== "m.replace") {
// Aggregating on last only makes sense for this relation type
return null;
}
if (!this._targetEvent) {
// Don't know which replacements to accept yet.
// This method shouldn't be called before the original
// event is known anyway.
return null;
}
return this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) {
return last;
}
if (last && last.getTs() > event.getTs()) {
return last;
}
return event;
}, null);
}
/*
* @param {MatrixEvent} targetEvent the event the relations are related to.
*/
setTargetEvent(event) {
if (this._targetEvent) {
return;
}
this._targetEvent = event;
if (this.relationType === "m.replace") {
const replacement = this.getLastReplacement();
// this is the initial update, so only call it if we already have something
// to not emit Event.replaced needlessly
if (replacement) {
this._targetEvent.makeReplaced(replacement);
}
}
}
}

View File

@@ -93,9 +93,12 @@ function synthesizeReceipt(userId, event, receiptType) {
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
@@ -1002,7 +1005,6 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
let i;
if (event.getType() === "m.room.redaction") {
const redactId = event.event.redacts;
@@ -1036,7 +1038,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
}
// add to our timeline sets
for (i = 0; i < this._timelineSets.length; i++) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
}
@@ -1101,9 +1103,27 @@ Room.prototype.addPendingEvent = function(event, txnId) {
if (this._opts.pendingEventOrdering == "detached") {
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.status = EventStatus.NOT_SENT;
event.setStatus(EventStatus.NOT_SENT);
}
this._pendingEventList.push(event);
if (event.isRelation()) {
// For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to
// the timeline set.)
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
timelineSet.aggregateRelations(event);
}
}
}
} else {
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
@@ -1242,7 +1262,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
newStatus);
}
event.status = newStatus;
event.setStatus(newStatus);
if (newStatus == EventStatus.SENT) {
// update the event id

View File

@@ -23,6 +23,37 @@ import {escapeRegExp, globToRegexp} from "./utils";
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
// The default override rules to apply when calculating actions for an event. These
// defaults apply under no other circumstances to avoid confusing the client with server
// state. We do this for two reasons:
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
// more details.
// 2. We often want to start using push rules ahead of the server supporting them,
// and so we can put them here.
const DEFAULT_OVERRIDE_RULES = [
{
// For homeservers which don't support MSC1930 yet
rule_id: ".m.rule.tombstone",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.tombstone",
},
],
actions: [
"notify",
{
set_tweak: "highlight",
value: true,
},
],
},
];
/**
* Construct a Push Processor.
* @constructor
@@ -312,6 +343,33 @@ function PushProcessor(client) {
return actionObj;
};
const applyRuleDefaults = function(clientRuleset) {
// Deep clone the object before we mutate it
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
if (!clientRuleset['global']) {
clientRuleset['global'] = {};
}
if (!clientRuleset['global']['override']) {
clientRuleset['global']['override'] = [];
}
// Apply default overrides
const globalOverrides = clientRuleset['global']['override'];
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (!existingRule) {
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return ruleset;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
@@ -331,7 +389,8 @@ function PushProcessor(client) {
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
const rules = applyRuleDefaults(client.pushRules);
return pushActionsForEventAndRulesets(ev, rules);
};
/**

View File

@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable no-invalid-this */
/* eslint-disable babel/no-invalid-this */
import Promise from 'bluebird';
import {MemoryStore} from "./memory";

View File

@@ -117,10 +117,15 @@ function SyncApi(client, opts) {
*/
SyncApi.prototype.createRoom = function(roomId) {
const client = this.client;
const {
timelineSupport,
unstableClientRelationAggregation,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
timelineSupport,
unstableClientRelationAggregation,
});
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
"Room.receipt", "Room.tags",
@@ -128,6 +133,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
]);
this._registerStateListeners(room);
return room;
@@ -712,7 +718,6 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
* @param {boolean} syncOptions.hasSyncedBefore
*/
SyncApi.prototype._sync = async function(syncOptions) {
debuglog("Starting sync request processing...");
const client = this.client;
if (!this._running) {
@@ -751,9 +756,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
// Reset after a successful sync
this._failedSyncCount = 0;
debuglog("Storing sync data...");
await client.store.setSyncData(data);
debuglog("Sync data stored");
const syncEventData = {
oldSyncToken: syncToken,
@@ -768,7 +771,6 @@ SyncApi.prototype._sync = async function(syncOptions) {
}
try {
debuglog("Processing sync response...");
await this._processSyncResponse(syncEventData, data);
} catch(e) {
// log the exception with stack if we have it, else fall back

746
yarn.lock

File diff suppressed because it is too large Load Diff