matrix-js-sdk
matrix-js-sdk copied to clipboard
Correct way to get all poll responses
Description: I am looking for the optimal way to get the latest results of a poll. My current approach is to initialize a "timelineWindow" at the poll index and paginate forward to get all relevant events. After that, I try to use the poll object to get all the responses, but it does not work reliably. If the poll is too far in the past, the response object doesn't contain all the responses or any responses at all. More recent polls work fine. The "timelineWindow" contains all emitted events, so I think the problem is the "getResponses()" function. Is it some kind of race condition?
What am I doing wrong? What is the correct way to get all of a poll's responses?
Thank you for your help!
Steps to Reproduce: See code below. "room" is the relevant room object. "pollID" is the ID of the relevant poll.
Expected Behavior: The "getResponses()" function should consistently retrieve all responses of the poll.
Actual Behavior: The "getResponses()" function sometimes retrieves all responses and sometimes does not. It's very unreliable.
Environment: matrix-js-sdk version: [30.2.0] Node.js version: [18.15.0]
Code:
async getPollResults(room, pollID) {
let timelineWindow = new TimelineWindow(this.MatrixClient, room?.getUnfilteredTimelineSet());
//fetch poll event
await timelineWindow.load(pollID);
//get responses till today. (https://github.com/matrix-org/matrix-js-sdk/issues/3001#issuecomment-1868109829)
let moreEventsAvailable = await timelineWindow.paginate(EventTimeline.FORWARDS, 8); //default is 8
while (moreEventsAvailable) {
moreEventsAvailable = await timelineWindow.paginate(EventTimeline.FORWARDS, 8); //default is 8
}
//"register" poll to room
await room?.processPollEvents(timelineWindow.getEvents());
let poll = room?.polls.get(pollID);
// console.log("Poll:", poll);
if (!poll) {
console.log("No poll:", poll);
return; // wrong Poll
}
let pollResult = poll.getResponses().then((responses) => {
console.log("responses", responses);
// console.log("responses.getRelations()", responses.getRelations());
if (!responses) {
// no responses??
return;
}
let unfilteredVotes = responses.getRelations().map((element) => {
let roomMember = room.getMember(element?.getSender()); //necessary to get correct name of sender
let Vote = {
ts: element?.getTs(),
sender: roomMember.userId,
senderName: roomMember.name,
answers: element?.event?.content["org.matrix.msc3381.poll.response"].answers,
};
return Vote;
});
// do more stuff with unfilteredVotes
return {
topic: poll.pollEvent.question.text,
userVotesMap: userVotes,
allVoteCount: allVoteCount,
voteCountMap: voteCountMap,
};
});
return pollResult;
}
To specify the problem: "poll.getResponses()" only retrieves the latest page of poll relations, resulting in the loss of earlier responses or vote changes. I don't see any way to specify options such as "to", "from", "limit" in "getResponses()" or "fetchResponses()" like in "MatrixClient.relations()" so I ended up writing my own "getResponses()"-function.
I removed the 'timelineWindow.paginate()' lines because they are irrelevant to this problem.
The following code solves my problem:
async getPollResults(room, pollID) {
let timelineWindow = new TimelineWindow(this.MatrixClient, room?.getUnfilteredTimelineSet());
// Fetch poll event
await timelineWindow.load(pollID);
// "Register" poll to room
await room?.processPollEvents(timelineWindow.getEvents());
let poll = room?.polls.get(pollID);
// console.log("Poll:", poll);
if (!poll) {
console.log("No poll:", poll);
return; // wrong Poll
}
// Get all responses
let allResponses = await this.getAllResponses(poll);
console.log("allResponses", allResponses);
console.log("allResponses.getRelations().length:", allResponses.getRelations().length);
if (!allResponses) {
console.log("No responses!", allResponses);
return;
}
let unfilteredVotes = allResponses.getRelations().map((element) => {
let roomMember = room.getMember(element?.getSender()); //necessary to get correct name of sender
// let answer = element?.event?.getContent();
let Vote = {
ts: element?.getTs(),
sender: roomMember.userId,
senderName: roomMember.name,
answers: M_POLL_RESPONSE.findIn(element?.event?.content).answers,
};
return Vote;
});
// do more stuff with unfilteredVotes
return {
topic: poll.pollEvent.question.text,
userVotesMap: userVotes,
allVoteCount: allVoteCount,
voteCountMap: voteCountMap,
};
}
async getAllResponses(poll) {
// Init responses:
let responses =
poll.responses ||
new Relations("m.reference", M_POLL_RESPONSE.name, poll.matrixClient, [M_POLL_RESPONSE.altName]);
// Get ALL relations:
let allRelations = await poll.matrixClient.relations(
poll.roomId,
poll.rootEvent.getId(),
"m.reference",
undefined,
{
limit: Number.MAX_SAFE_INTEGER,
}
);
await Promise.all(allRelations.events.map((event) => poll.matrixClient.decryptEventIfNeeded(event)));
console.log("allRelations.events.length:", allRelations.events.length);
// Get first M_POLL_END event
let pollEndEvents = allRelations.events.filter((event) => M_POLL_END.matches(event.getType()));
if (pollEndEvents.length > 1) {
pollEndEvents.sort((a, b) => a.getTs() - b.getTs());
}
let pollCloseTimestamp = pollEndEvents[0] ? pollEndEvents[0].getTs() : Number.MAX_SAFE_INTEGER;
// Filter all relations to get valid responses
let { responseEvents } = this.filterResponseRelations(allRelations.events, pollCloseTimestamp);
responseEvents.forEach((event) => {
responses.addEvent(event);
});
return responses;
}
filterResponseRelations = (relationEvents, pollEndTimestamp) => {
const responseEvents = relationEvents.filter((event) => {
if (event.isDecryptionFailure()) {
return;
}
return (
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,
};
};
If you know of a better approach, please don't hesitate to inform me.