243 lines
9.1 KiB
JavaScript
243 lines
9.1 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.isPollEvent = exports.PollEvent = exports.Poll = void 0;
|
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
var _matrixEventsSdk = require("matrix-events-sdk");
|
|
var _polls = require("../@types/polls");
|
|
var _relations = require("./relations");
|
|
var _typedEventEmitter = require("./typed-event-emitter");
|
|
/*
|
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
let PollEvent = /*#__PURE__*/function (PollEvent) {
|
|
PollEvent["New"] = "Poll.new";
|
|
PollEvent["End"] = "Poll.end";
|
|
PollEvent["Update"] = "Poll.update";
|
|
PollEvent["Responses"] = "Poll.Responses";
|
|
PollEvent["Destroy"] = "Poll.Destroy";
|
|
PollEvent["UndecryptableRelations"] = "Poll.UndecryptableRelations";
|
|
return PollEvent;
|
|
}({});
|
|
exports.PollEvent = PollEvent;
|
|
const filterResponseRelations = (relationEvents, pollEndTimestamp) => {
|
|
const responseEvents = relationEvents.filter(event => {
|
|
if (event.isDecryptionFailure()) {
|
|
return;
|
|
}
|
|
return _polls.M_POLL_RESPONSE.matches(event.getType()) &&
|
|
// From MSC3381:
|
|
// "Votes sent on or before the end event's timestamp are valid votes"
|
|
event.getTs() <= pollEndTimestamp;
|
|
});
|
|
return {
|
|
responseEvents
|
|
};
|
|
};
|
|
class Poll extends _typedEventEmitter.TypedEventEmitter {
|
|
constructor(rootEvent, matrixClient, room) {
|
|
super();
|
|
this.rootEvent = rootEvent;
|
|
this.matrixClient = matrixClient;
|
|
this.room = room;
|
|
(0, _defineProperty2.default)(this, "roomId", void 0);
|
|
(0, _defineProperty2.default)(this, "pollEvent", void 0);
|
|
(0, _defineProperty2.default)(this, "_isFetchingResponses", false);
|
|
(0, _defineProperty2.default)(this, "relationsNextBatch", void 0);
|
|
(0, _defineProperty2.default)(this, "responses", null);
|
|
(0, _defineProperty2.default)(this, "endEvent", void 0);
|
|
/**
|
|
* Keep track of undecryptable relations
|
|
* As incomplete result sets affect poll results
|
|
*/
|
|
(0, _defineProperty2.default)(this, "undecryptableRelationEventIds", new Set());
|
|
(0, _defineProperty2.default)(this, "countUndecryptableEvents", events => {
|
|
const undecryptableEventIds = events.filter(event => event.isDecryptionFailure()).map(event => event.getId());
|
|
const previousCount = this.undecryptableRelationsCount;
|
|
this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]);
|
|
if (this.undecryptableRelationsCount !== previousCount) {
|
|
this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount);
|
|
}
|
|
});
|
|
if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
|
|
throw new Error("Invalid poll start event.");
|
|
}
|
|
this.roomId = this.rootEvent.getRoomId();
|
|
this.pollEvent = this.rootEvent.unstableExtensibleEvent;
|
|
}
|
|
get pollId() {
|
|
return this.rootEvent.getId();
|
|
}
|
|
get endEventId() {
|
|
var _this$endEvent;
|
|
return (_this$endEvent = this.endEvent) === null || _this$endEvent === void 0 ? void 0 : _this$endEvent.getId();
|
|
}
|
|
get isEnded() {
|
|
return !!this.endEvent;
|
|
}
|
|
get isFetchingResponses() {
|
|
return this._isFetchingResponses;
|
|
}
|
|
get undecryptableRelationsCount() {
|
|
return this.undecryptableRelationEventIds.size;
|
|
}
|
|
async getResponses() {
|
|
// if we have already fetched some responses
|
|
// just return them
|
|
if (this.responses) {
|
|
return this.responses;
|
|
}
|
|
|
|
// if there is no fetching in progress
|
|
// start fetching
|
|
if (!this.isFetchingResponses) {
|
|
await this.fetchResponses();
|
|
}
|
|
// return whatever responses we got from the first page
|
|
return this.responses;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param event - event with a relation to the rootEvent
|
|
* @returns void
|
|
*/
|
|
onNewRelation(event) {
|
|
var _this$endEvent2;
|
|
if (_polls.M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) {
|
|
this.endEvent = event;
|
|
this.refilterResponsesOnEnd();
|
|
this.emit(PollEvent.End);
|
|
}
|
|
|
|
// wait for poll responses to be initialised
|
|
if (!this.responses) {
|
|
return;
|
|
}
|
|
const pollEndTimestamp = ((_this$endEvent2 = this.endEvent) === null || _this$endEvent2 === void 0 ? void 0 : _this$endEvent2.getTs()) || Number.MAX_SAFE_INTEGER;
|
|
const {
|
|
responseEvents
|
|
} = filterResponseRelations([event], pollEndTimestamp);
|
|
this.countUndecryptableEvents([event]);
|
|
if (responseEvents.length) {
|
|
responseEvents.forEach(event => {
|
|
this.responses.addEvent(event);
|
|
});
|
|
this.emit(PollEvent.Responses, this.responses);
|
|
}
|
|
}
|
|
async fetchResponses() {
|
|
var _this$endEvent3, _allRelations$nextBat;
|
|
this._isFetchingResponses = true;
|
|
|
|
// we want:
|
|
// - stable and unstable M_POLL_RESPONSE
|
|
// - stable and unstable M_POLL_END
|
|
// so make one api call and filter by event type client side
|
|
const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId(), "m.reference", undefined, {
|
|
from: this.relationsNextBatch || undefined
|
|
});
|
|
await Promise.all(allRelations.events.map(event => this.matrixClient.decryptEventIfNeeded(event)));
|
|
const responses = this.responses || new _relations.Relations("m.reference", _polls.M_POLL_RESPONSE.name, this.matrixClient, [_polls.M_POLL_RESPONSE.altName]);
|
|
const pollEndEvent = allRelations.events.find(event => _polls.M_POLL_END.matches(event.getType()));
|
|
if (this.validateEndEvent(pollEndEvent)) {
|
|
this.endEvent = pollEndEvent;
|
|
this.refilterResponsesOnEnd();
|
|
this.emit(PollEvent.End);
|
|
}
|
|
const pollCloseTimestamp = ((_this$endEvent3 = this.endEvent) === null || _this$endEvent3 === void 0 ? void 0 : _this$endEvent3.getTs()) || Number.MAX_SAFE_INTEGER;
|
|
const {
|
|
responseEvents
|
|
} = filterResponseRelations(allRelations.events, pollCloseTimestamp);
|
|
responseEvents.forEach(event => {
|
|
responses.addEvent(event);
|
|
});
|
|
this.relationsNextBatch = (_allRelations$nextBat = allRelations.nextBatch) !== null && _allRelations$nextBat !== void 0 ? _allRelations$nextBat : undefined;
|
|
this.responses = responses;
|
|
this.countUndecryptableEvents(allRelations.events);
|
|
|
|
// while there are more pages of relations
|
|
// fetch them
|
|
if (this.relationsNextBatch) {
|
|
// don't await
|
|
// we want to return the first page as soon as possible
|
|
this.fetchResponses();
|
|
} else {
|
|
// no more pages
|
|
this._isFetchingResponses = false;
|
|
}
|
|
|
|
// emit after updating _isFetchingResponses state
|
|
this.emit(PollEvent.Responses, this.responses);
|
|
}
|
|
|
|
/**
|
|
* Only responses made before the poll ended are valid
|
|
* Refilter after an end event is recieved
|
|
* To ensure responses are valid
|
|
*/
|
|
refilterResponsesOnEnd() {
|
|
var _this$endEvent4;
|
|
if (!this.responses) {
|
|
return;
|
|
}
|
|
const pollEndTimestamp = ((_this$endEvent4 = this.endEvent) === null || _this$endEvent4 === void 0 ? void 0 : _this$endEvent4.getTs()) || Number.MAX_SAFE_INTEGER;
|
|
this.responses.getRelations().forEach(event => {
|
|
if (event.getTs() > pollEndTimestamp) {
|
|
var _this$responses;
|
|
(_this$responses = this.responses) === null || _this$responses === void 0 ? void 0 : _this$responses.removeEvent(event);
|
|
}
|
|
});
|
|
this.emit(PollEvent.Responses, this.responses);
|
|
}
|
|
validateEndEvent(endEvent) {
|
|
if (!endEvent) {
|
|
return false;
|
|
}
|
|
/**
|
|
* Repeated end events are ignored -
|
|
* only the first (valid) closure event by origin_server_ts is counted.
|
|
*/
|
|
if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* MSC3381
|
|
* If a m.poll.end event is received from someone other than the poll creator or user with permission to redact
|
|
* others' messages in the room, the event must be ignored by clients due to being invalid.
|
|
*/
|
|
const roomCurrentState = this.room.currentState;
|
|
const endEventSender = endEvent.getSender();
|
|
return !!endEventSender && (endEventSender === this.rootEvent.getSender() || roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tests whether the event is a start, response or end poll event.
|
|
*
|
|
* @param event - Event to test
|
|
* @returns true if the event is a poll event, else false
|
|
*/
|
|
exports.Poll = Poll;
|
|
const isPollEvent = event => {
|
|
const eventType = event.getType();
|
|
return _matrixEventsSdk.M_POLL_START.matches(eventType) || _polls.M_POLL_RESPONSE.matches(eventType) || _polls.M_POLL_END.matches(eventType);
|
|
};
|
|
exports.isPollEvent = isPollEvent;
|
|
//# sourceMappingURL=poll.js.map
|