Skip to content

Commit

Permalink
Merge branch 'travis/extev-polls' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
turt2live committed Jan 13, 2022
2 parents 30e4f10 + 9e289c8 commit a1c93eb
Show file tree
Hide file tree
Showing 18 changed files with 1,558 additions and 10 deletions.
5 changes: 5 additions & 0 deletions src/ExtensibleEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { InvalidEventError } from "./InvalidEventError";
import { LEGACY_M_ROOM_MESSAGE, parseMRoomMessage } from "./interpreters/legacy/MRoomMessage";
import { parseMMessage } from "./interpreters/modern/MMessage";
import { M_EMOTE, M_MESSAGE, M_NOTICE } from "./events/message_types";
import { M_POLL_END, M_POLL_RESPONSE, M_POLL_START } from "./events/poll_types";
import { parseMPoll } from "./interpreters/modern/MPoll";

export type EventInterpreter<TContentIn = object, TEvent extends ExtensibleEvent = ExtensibleEvent>
= (wireEvent: IPartialEvent<TContentIn>) => Optional<TEvent>;
Expand All @@ -41,6 +43,9 @@ export class ExtensibleEvents {
[M_MESSAGE, parseMMessage],
[M_EMOTE, parseMMessage],
[M_NOTICE, parseMMessage],
[M_POLL_START, parseMPoll],
[M_POLL_RESPONSE, parseMPoll],
[M_POLL_END, parseMPoll],
]);

private _unknownInterpretOrder: NamespacedValue<string, string>[] = [
Expand Down
20 changes: 19 additions & 1 deletion src/events/MessageEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,29 @@ export class MessageEvent extends ExtensibleEvent<M_MESSAGE_EVENT_CONTENT> {
return M_NOTICE.matches(this.wireFormat.type) || isProvided(M_NOTICE.findIn(this.wireFormat.content));
}

protected serializeMMessageOnly(): M_MESSAGE_EVENT_CONTENT {
let messageRendering: M_MESSAGE_EVENT_CONTENT = {
[M_MESSAGE.name]: this.renderings,
};

// Use the shorthand if it's just a simple text event
if (this.renderings.length === 1) {
const mime = this.renderings[0].mimetype;
if (mime === undefined || mime === "text/plain") {
messageRendering = {
[M_TEXT.name]: this.renderings[0].body,
};
}
}

return messageRendering;
}

public serialize(): IPartialEvent<object> {
return {
type: "m.room.message",
content: {
[M_MESSAGE.name]: this.renderings,
...this.serializeMMessageOnly(),
body: this.text,
msgtype: "m.text",
format: this.html ? "org.matrix.custom.html" : undefined,
Expand Down
89 changes: 89 additions & 0 deletions src/events/PollEndEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
Copyright 2022 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.
*/

import { ExtensibleEvent } from "./ExtensibleEvent";
import { M_POLL_END_EVENT_CONTENT, M_POLL_END } from "./poll_types";
import { IPartialEvent } from "../IPartialEvent";
import { InvalidEventError } from "../InvalidEventError";
import { REFERENCE_RELATION } from "./relationship_types";
import { MessageEvent } from "./MessageEvent";
import { M_TEXT } from "./message_types";

/**
* Represents a poll end/closure event.
*/
export class PollEndEvent extends ExtensibleEvent<M_POLL_END_EVENT_CONTENT> {
/**
* The poll start event ID referenced by the response.
*/
public readonly pollEventId: string;

/**
* The closing message for the event.
*/
public readonly closingMessage: MessageEvent;

/**
* Creates a new PollEndEvent from a pure format. Note that the event is *not*
* parsed here: it will be treated as a literal m.poll.response primary typed event.
* @param {IPartialEvent<M_POLL_END_EVENT_CONTENT>} wireFormat The event.
*/
public constructor(wireFormat: IPartialEvent<M_POLL_END_EVENT_CONTENT>) {
super(wireFormat);

const rel = this.wireContent["m.relates_to"];
if (!REFERENCE_RELATION.matches(rel?.rel_type) || (typeof rel?.event_id) !== "string") {
throw new InvalidEventError("Relationship must be a reference to an event");
}

this.pollEventId = rel.event_id;
this.closingMessage = new MessageEvent(this.wireFormat);
}

public serialize(): IPartialEvent<object> {
return {
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: this.pollEventId,
},
[M_POLL_END.name]: {},
...this.closingMessage.serialize().content,
},
};
}

/**
* Creates a new PollEndEvent from a poll event ID.
* @param {string} pollEventId The poll start event ID.
* @param {string} message A closing message, typically revealing the top answer.
* @returns {PollStartEvent} The representative poll closure event.
*/
public static from(pollEventId: string, message: string): PollEndEvent {
return new PollEndEvent({
type: M_POLL_END.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: pollEventId,
},
[M_POLL_END.name]: {},
[M_TEXT.name]: message,
},
});
}
}
140 changes: 140 additions & 0 deletions src/events/PollResponseEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
Copyright 2022 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.
*/

import { ExtensibleEvent } from "./ExtensibleEvent";
import { M_POLL_RESPONSE, M_POLL_RESPONSE_EVENT_CONTENT, M_POLL_RESPONSE_SUBTYPE } from "./poll_types";
import { IPartialEvent } from "../IPartialEvent";
import { InvalidEventError } from "../InvalidEventError";
import { PollStartEvent } from "./PollStartEvent";
import { REFERENCE_RELATION } from "./relationship_types";

/**
* Represents a poll response event.
*/
export class PollResponseEvent extends ExtensibleEvent<M_POLL_RESPONSE_EVENT_CONTENT> {
private internalAnswerIds: string[];
private internalSpoiled: boolean;

/**
* The provided answers for the poll. Note that this may be falsy/unpredictable if
* the `spoiled` property is true.
*/
public get answerIds(): string[] {
return this.internalAnswerIds;
}

/**
* The poll start event ID referenced by the response.
*/
public readonly pollEventId: string;

/**
* Whether the vote is spoiled.
*/
public get spoiled(): boolean {
return this.internalSpoiled;
}

/**
* Creates a new PollResponseEvent from a pure format. Note that the event is *not*
* parsed here: it will be treated as a literal m.poll.response primary typed event.
*
* To validate the response against a poll, call `validateAgainst` after creation.
* @param {IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT>} wireFormat The event.
*/
public constructor(wireFormat: IPartialEvent<M_POLL_RESPONSE_EVENT_CONTENT>) {
super(wireFormat);

const rel = this.wireContent["m.relates_to"];
if (!REFERENCE_RELATION.matches(rel?.rel_type) || (typeof rel?.event_id) !== "string") {
throw new InvalidEventError("Relationship must be a reference to an event");
}

this.pollEventId = rel.event_id;
this.validateAgainst(null);
}

/**
* Validates the poll response using the poll start event as a frame of reference. This
* is used to determine if the vote is spoiled, whether the answers are valid, etc.
* @param {PollStartEvent} poll The poll start event.
*/
public validateAgainst(poll: PollStartEvent) {
const response = M_POLL_RESPONSE.findIn<M_POLL_RESPONSE_SUBTYPE>(this.wireContent);
if (!Array.isArray(response?.answers)) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}

let answers = response.answers;
if (answers.some(a => (typeof a) !== "string") || answers.length === 0) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}

if (poll) {
if (answers.some(a => !poll.answers.some(pa => pa.id === a))) {
this.internalSpoiled = true;
this.internalAnswerIds = [];
return;
}

answers = answers.slice(0, poll.maxSelections);
}

this.internalAnswerIds = answers;
this.internalSpoiled = false;
}

public serialize(): IPartialEvent<object> {
return {
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: this.pollEventId,
},
[M_POLL_RESPONSE.name]: {
answers: this.spoiled ? undefined : this.answerIds,
},
},
};
}

/**
* Creates a new PollResponseEvent from a set of answers. To spoil the vote, pass an empty
* answers array.
* @param {string} answers The user's answers. Should be valid from a poll's answer IDs.
* @param {string} pollEventId The poll start event ID.
* @returns {PollStartEvent} The representative poll response event.
*/
public static from(answers: string[], pollEventId: string): PollResponseEvent {
return new PollResponseEvent({
type: M_POLL_RESPONSE.name,
content: {
"m.relates_to": {
rel_type: REFERENCE_RELATION.name,
event_id: pollEventId,
},
[M_POLL_RESPONSE.name]: {
answers: answers,
},
},
});
}
}
Loading

0 comments on commit a1c93eb

Please sign in to comment.