Line data Source code
1 : import 'package:matrix/matrix.dart';
2 : import 'package:matrix/msc_extensions/msc_3381_polls/models/poll_event_content.dart';
3 :
4 : extension PollEventExtension on Event {
5 2 : PollEventContent get parsedPollEventContent {
6 4 : assert(type == PollEventContent.startType);
7 4 : return PollEventContent.fromJson(content);
8 : }
9 :
10 : /// Returns a Map of answer IDs to a Set of user IDs.
11 2 : Map<String, Set<String>> getPollResponses(Timeline timeline) {
12 4 : assert(type == PollEventContent.startType);
13 : final aggregatedEvents =
14 10 : timeline.aggregatedEvents[eventId]?['m.reference']?.toList();
15 4 : if (aggregatedEvents == null || aggregatedEvents.isEmpty) return {};
16 :
17 2 : final responses = <String, Set<String>>{};
18 :
19 6 : final maxSelection = parsedPollEventContent.pollStartContent.maxSelections;
20 :
21 4 : aggregatedEvents.removeWhere((event) {
22 4 : if (event.type != PollEventContent.responseType) return true;
23 :
24 : // Votes with timestamps after the poll has closed are ignored, as if they
25 : // never happened.
26 6 : if (originServerTs.isAfter(event.originServerTs)) {
27 0 : Logs().d('Ignore poll answer which came after poll was closed.');
28 : return true;
29 : }
30 :
31 2 : final answers = event.content
32 2 : .tryGetMap<String, Object?>(PollEventContent.responseType)
33 2 : ?.tryGetList<String>('answers');
34 : if (answers == null) {
35 0 : Logs().d('Ignore poll answer with now valid answer IDs');
36 : return true;
37 : }
38 4 : if (answers.length > maxSelection) {
39 0 : Logs().d(
40 0 : 'Ignore poll answer with ${answers.length} while only $maxSelection are allowed.',
41 : );
42 : return true;
43 : }
44 : return false;
45 : });
46 :
47 : // Sort by date so only the users most recent vote is used in the end, even
48 : // if it is invalid.
49 : aggregatedEvents
50 2 : .sort((a, b) => a.originServerTs.compareTo(b.originServerTs));
51 :
52 4 : for (final event in aggregatedEvents) {
53 2 : final answers = event.content
54 2 : .tryGetMap<String, Object?>(PollEventContent.responseType)
55 2 : ?.tryGetList<String>('answers') ??
56 0 : [];
57 6 : responses[event.senderId] = answers.toSet();
58 : }
59 : return responses;
60 : }
61 :
62 2 : bool getPollHasBeenEnded(Timeline timeline) {
63 4 : assert(type == PollEventContent.startType);
64 8 : final aggregatedEvents = timeline.aggregatedEvents[eventId]?['m.reference'];
65 2 : if (aggregatedEvents == null || aggregatedEvents.isEmpty) return false;
66 :
67 2 : final redactPowerLevel = (room
68 2 : .getState(EventTypes.RoomPowerLevels)
69 0 : ?.content
70 0 : .tryGet<int>('redact') ??
71 : 50);
72 :
73 2 : return aggregatedEvents.any(
74 2 : (event) {
75 2 : if (event.content
76 2 : .tryGetMap<String, Object?>(PollEventContent.endType) ==
77 : null) {
78 : return false;
79 : }
80 :
81 : // If a m.poll.end event is received from someone other than the poll
82 : //creator or user with permission to redact other's messages in the
83 : //room, the event must be ignored by clients due to being invalid.
84 6 : if (event.senderId == senderId ||
85 0 : event.senderFromMemoryOrFallback.powerLevel >= redactPowerLevel) {
86 : return true;
87 : }
88 0 : Logs().w(
89 0 : 'Ignore poll end event form user without permission ${event.senderId}',
90 : );
91 : return false;
92 : },
93 : );
94 : }
95 :
96 2 : Future<String?> answerPoll(
97 : List<String> answerIds, {
98 : String? txid,
99 : }) {
100 6 : final maxSelection = parsedPollEventContent.pollStartContent.maxSelections;
101 4 : if (answerIds.length > maxSelection) {
102 0 : throw Exception(
103 0 : 'Can not add ${answerIds.length} answers while max selection is $maxSelection',
104 : );
105 : }
106 4 : return room.sendEvent(
107 2 : {
108 2 : 'm.relates_to': {
109 : 'rel_type': 'm.reference',
110 2 : 'event_id': eventId,
111 : },
112 2 : PollEventContent.responseType: {'answers': answerIds},
113 : },
114 : type: PollEventContent.responseType,
115 : txid: txid,
116 : );
117 : }
118 :
119 0 : Future<String?> endPoll({String? txid}) => room.sendEvent(
120 0 : {
121 0 : 'm.relates_to': {
122 : 'rel_type': 'm.reference',
123 0 : 'event_id': eventId,
124 : },
125 0 : PollEventContent.endType: {},
126 : },
127 : type: PollEventContent.endType,
128 : txid: txid,
129 : );
130 : }
|