Skip to content

Commit a1927ae

Browse files
committed
Add sticky event support to the js-sdk
Signed-off-by: Timo K <toger5@hotmail.de>
1 parent dbe441d commit a1927ae

File tree

8 files changed

+250
-56
lines changed

8 files changed

+250
-56
lines changed

src/@types/event.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,9 @@ export interface TimelineEvents {
337337
[M_BEACON.name]: MBeaconEventContent;
338338
[M_POLL_START.name]: PollStartEventContent;
339339
[M_POLL_END.name]: PollEndEventContent;
340+
// MSC3401 Adding this to the timeline events as well for sending this event as a sticky event.
341+
// { sticky_key: string } is the empty object but we always need a sticky key
342+
[EventType.GroupCallMemberPrefix]: SessionMembershipData | EmptyObject;
340343
}
341344

342345
/**

src/@types/requests.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ export type SendActionDelayedEventRequestOpts = ParentDelayId;
107107

108108
export type SendDelayedEventRequestOpts = SendTimeoutDelayedEventRequestOpts | SendActionDelayedEventRequestOpts;
109109

110+
export function isSendDelayedEventRequestOpts(opts: object): opts is SendDelayedEventRequestOpts {
111+
return (opts as TimeoutDelay).delay !== undefined || (opts as ParentDelayId).parent_delay_id !== undefined;
112+
}
110113
export type SendDelayedEventResponse = {
111114
delay_id: string;
112115
};

src/client.ts

Lines changed: 142 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import {
105105
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
106106
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
107107
import {
108+
isSendDelayedEventRequestOpts,
108109
type DelayedEventInfo,
109110
type IAddThreePidOnlyBody,
110111
type IBindThreePidBody,
@@ -540,6 +541,7 @@ export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms"
540541
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
541542

542543
export const UNSTABLE_MSC4140_DELAYED_EVENTS = "org.matrix.msc4140";
544+
export const UNSTABLE_MSC4354_STICKY_EVENTS = "org.matrix.msc4354";
543545

544546
export const UNSTABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133";
545547
export const STABLE_MSC4133_EXTENDED_PROFILES = "uk.tcpip.msc4133.stable";
@@ -2663,7 +2665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
26632665
}
26642666

26652667
this.addThreadRelationIfNeeded(content, threadId, roomId);
2666-
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, txnId);
2668+
return this.sendCompleteEvent({ roomId, threadId, eventObject: { type: eventType, content }, txnId });
26672669
}
26682670

26692671
/**
@@ -2700,12 +2702,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
27002702
* @returns Promise which resolves: to an empty object `{}`
27012703
* @returns Rejects: with an error response.
27022704
*/
2703-
private sendCompleteEvent(
2704-
roomId: string,
2705-
threadId: string | null,
2706-
eventObject: Partial<IEvent>,
2707-
txnId?: string,
2708-
): Promise<ISendEventResponse>;
2705+
private sendCompleteEvent(params: {
2706+
roomId: string;
2707+
threadId: string | null;
2708+
eventObject: Partial<IEvent>;
2709+
queryDict?: QueryDict;
2710+
txnId?: string;
2711+
}): Promise<ISendEventResponse>;
27092712
/**
27102713
* Sends a delayed event (MSC4140).
27112714
* @param eventObject - An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
@@ -2714,29 +2717,29 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
27142717
* @returns Promise which resolves: to an empty object `{}`
27152718
* @returns Rejects: with an error response.
27162719
*/
2717-
private sendCompleteEvent(
2718-
roomId: string,
2719-
threadId: string | null,
2720-
eventObject: Partial<IEvent>,
2721-
delayOpts: SendDelayedEventRequestOpts,
2722-
txnId?: string,
2723-
): Promise<SendDelayedEventResponse>;
2724-
private sendCompleteEvent(
2725-
roomId: string,
2726-
threadId: string | null,
2727-
eventObject: Partial<IEvent>,
2728-
delayOptsOrTxnId?: SendDelayedEventRequestOpts | string,
2729-
txnIdOrVoid?: string,
2730-
): Promise<ISendEventResponse | SendDelayedEventResponse> {
2731-
let delayOpts: SendDelayedEventRequestOpts | undefined;
2732-
let txnId: string | undefined;
2733-
if (typeof delayOptsOrTxnId === "string") {
2734-
txnId = delayOptsOrTxnId;
2735-
} else {
2736-
delayOpts = delayOptsOrTxnId;
2737-
txnId = txnIdOrVoid;
2738-
}
2739-
2720+
private sendCompleteEvent(params: {
2721+
roomId: string;
2722+
threadId: string | null;
2723+
eventObject: Partial<IEvent>;
2724+
delayOpts: SendDelayedEventRequestOpts;
2725+
queryDict?: QueryDict;
2726+
txnId?: string;
2727+
}): Promise<SendDelayedEventResponse>;
2728+
private sendCompleteEvent({
2729+
roomId,
2730+
threadId,
2731+
eventObject,
2732+
delayOpts,
2733+
queryDict,
2734+
txnId,
2735+
}: {
2736+
roomId: string;
2737+
threadId: string | null;
2738+
eventObject: Partial<IEvent>;
2739+
delayOpts?: SendDelayedEventRequestOpts;
2740+
queryDict?: QueryDict;
2741+
txnId?: string;
2742+
}): Promise<SendDelayedEventResponse | ISendEventResponse> {
27402743
if (!txnId) {
27412744
txnId = this.makeTxnId();
27422745
}
@@ -2779,7 +2782,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
27792782

27802783
const type = localEvent.getType();
27812784
this.logger.debug(
2782-
`sendEvent of type ${type} in ${roomId} with txnId ${txnId}${delayOpts ? " (delayed event)" : ""}`,
2785+
`sendEvent of type ${type} in ${roomId} with txnId ${txnId}${delayOpts ? " (delayed event)" : ""}${queryDict ? " query params: " + JSON.stringify(queryDict) : ""}`,
27832786
);
27842787

27852788
localEvent.setTxnId(txnId);
@@ -2797,17 +2800,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
27972800
return Promise.reject(new Error("Event blocked by other events not yet sent"));
27982801
}
27992802

2800-
return this.encryptAndSendEvent(room, localEvent);
2803+
return this.encryptAndSendEvent(room, localEvent, queryDict);
28012804
} else {
2802-
return this.encryptAndSendEvent(room, localEvent, delayOpts);
2805+
return this.encryptAndSendEvent(room, localEvent, delayOpts, queryDict);
28032806
}
28042807
}
28052808

28062809
/**
28072810
* encrypts the event if necessary; adds the event to the queue, or sends it; marks the event as sent/unsent
28082811
* @returns returns a promise which resolves with the result of the send request
28092812
*/
2810-
protected async encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise<ISendEventResponse>;
2813+
protected async encryptAndSendEvent(
2814+
room: Room | null,
2815+
event: MatrixEvent,
2816+
queryDict?: QueryDict,
2817+
): Promise<ISendEventResponse>;
28112818
/**
28122819
* Simply sends a delayed event without encrypting it.
28132820
* TODO: Allow encrypted delayed events, and encrypt them properly
@@ -2818,16 +2825,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
28182825
room: Room | null,
28192826
event: MatrixEvent,
28202827
delayOpts: SendDelayedEventRequestOpts,
2821-
): Promise<SendDelayedEventResponse>;
2828+
queryDict?: QueryDict,
2829+
): Promise<ISendEventResponse>;
28222830
protected async encryptAndSendEvent(
28232831
room: Room | null,
28242832
event: MatrixEvent,
2825-
delayOpts?: SendDelayedEventRequestOpts,
2833+
delayOptsOrQuery?: SendDelayedEventRequestOpts | QueryDict,
2834+
queryDict?: QueryDict,
28262835
): Promise<ISendEventResponse | SendDelayedEventResponse> {
2827-
if (delayOpts) {
2828-
return this.sendEventHttpRequest(event, delayOpts);
2836+
let queryOpts = queryDict;
2837+
if (delayOptsOrQuery && isSendDelayedEventRequestOpts(delayOptsOrQuery)) {
2838+
return this.sendEventHttpRequest(event, delayOptsOrQuery, queryOpts);
2839+
} else if (!queryOpts) {
2840+
queryOpts = delayOptsOrQuery;
28292841
}
2830-
28312842
try {
28322843
let cancelled: boolean;
28332844
this.eventsBeingEncrypted.add(event.getId()!);
@@ -2863,7 +2874,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
28632874
}
28642875

28652876
if (!promise) {
2866-
promise = this.sendEventHttpRequest(event);
2877+
promise = this.sendEventHttpRequest(event, queryOpts);
28672878
if (room) {
28682879
promise = promise.then((res) => {
28692880
room.updatePendingEvent(event, EventStatus.SENT, res["event_id"]);
@@ -2978,14 +2989,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
29782989
}
29792990
}
29802991

2981-
private sendEventHttpRequest(event: MatrixEvent): Promise<ISendEventResponse>;
2992+
private sendEventHttpRequest(event: MatrixEvent, queryDict?: QueryDict): Promise<ISendEventResponse>;
29822993
private sendEventHttpRequest(
29832994
event: MatrixEvent,
29842995
delayOpts: SendDelayedEventRequestOpts,
2996+
queryDict?: QueryDict,
29852997
): Promise<SendDelayedEventResponse>;
29862998
private sendEventHttpRequest(
29872999
event: MatrixEvent,
2988-
delayOpts?: SendDelayedEventRequestOpts,
3000+
queryOrDelayOpts?: SendDelayedEventRequestOpts | QueryDict,
3001+
queryDict?: QueryDict,
29893002
): Promise<ISendEventResponse | SendDelayedEventResponse> {
29903003
let txnId = event.getTxnId();
29913004
if (!txnId) {
@@ -3018,19 +3031,22 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
30183031
path = utils.encodeUri("/rooms/$roomId/send/$eventType/$txnId", pathParams);
30193032
}
30203033

3034+
const delayOpts =
3035+
queryOrDelayOpts && isSendDelayedEventRequestOpts(queryOrDelayOpts) ? queryOrDelayOpts : undefined;
3036+
const queryOpts = !delayOpts ? queryOrDelayOpts : queryDict;
30213037
const content = event.getWireContent();
3022-
if (!delayOpts) {
3023-
return this.http.authedRequest<ISendEventResponse>(Method.Put, path, undefined, content).then((res) => {
3024-
this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
3025-
return res;
3026-
});
3027-
} else {
3038+
if (delayOpts) {
30283039
return this.http.authedRequest<SendDelayedEventResponse>(
30293040
Method.Put,
30303041
path,
3031-
getUnstableDelayQueryOpts(delayOpts),
3042+
{ ...getUnstableDelayQueryOpts(delayOpts), ...queryOpts },
30323043
content,
30333044
);
3045+
} else {
3046+
return this.http.authedRequest<ISendEventResponse>(Method.Put, path, queryOpts, content).then((res) => {
3047+
this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
3048+
return res;
3049+
});
30343050
}
30353051
}
30363052

@@ -3087,16 +3103,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
30873103
content[withRelTypesPropName] = opts.with_rel_types;
30883104
}
30893105

3090-
return this.sendCompleteEvent(
3106+
return this.sendCompleteEvent({
30913107
roomId,
30923108
threadId,
3093-
{
3109+
eventObject: {
30943110
type: EventType.RoomRedaction,
30953111
content,
30963112
redacts: eventId,
30973113
},
3098-
txnId as string,
3099-
);
3114+
txnId: txnId as string,
3115+
});
31003116
}
31013117

31023118
/**
@@ -3384,7 +3400,47 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
33843400
}
33853401

33863402
this.addThreadRelationIfNeeded(content, threadId, roomId);
3387-
return this.sendCompleteEvent(roomId, threadId, { type: eventType, content }, delayOpts, txnId);
3403+
return this.sendCompleteEvent({
3404+
roomId,
3405+
threadId,
3406+
eventObject: { type: eventType, content },
3407+
delayOpts,
3408+
txnId,
3409+
});
3410+
}
3411+
3412+
/**
3413+
* Send a delayed timeline event.
3414+
*
3415+
* Note: This endpoint is unstable, and can throw an `Error`.
3416+
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
3417+
*/
3418+
// eslint-disable-next-line
3419+
public async _unstable_sendStickyDelayedEvent<K extends keyof TimelineEvents>(
3420+
roomId: string,
3421+
stickDuration: number,
3422+
delayOpts: SendDelayedEventRequestOpts,
3423+
threadId: string | null,
3424+
eventType: K,
3425+
content: TimelineEvents[K] & { sticky_key: string },
3426+
txnId?: string,
3427+
): Promise<SendDelayedEventResponse> {
3428+
// if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS))) {
3429+
// throw new UnsupportedStickyEventsEndpointError(
3430+
// "Server does not support the sticky events",
3431+
// "sendStickyEvent",
3432+
// );
3433+
// }
3434+
3435+
this.addThreadRelationIfNeeded(content, threadId, roomId);
3436+
return this.sendCompleteEvent({
3437+
roomId,
3438+
threadId,
3439+
eventObject: { type: eventType, content },
3440+
queryDict: { msc4354_stick_duration_ms: stickDuration },
3441+
delayOpts,
3442+
txnId,
3443+
});
33883444
}
33893445

33903446
/**
@@ -3421,6 +3477,38 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
34213477
return this.http.authedRequest(Method.Put, path, getUnstableDelayQueryOpts(delayOpts), content as Body, opts);
34223478
}
34233479

3480+
/**
3481+
* Send a delayed timeline event.
3482+
*
3483+
* Note: This endpoint is unstable, and can throw an `Error`.
3484+
* Check progress on [MSC4140](https://github.com/matrix-org/matrix-spec-proposals/pull/4140) for more details.
3485+
*/
3486+
// eslint-disable-next-line
3487+
public async _unstable_sendStickyEvent<K extends keyof TimelineEvents>(
3488+
roomId: string,
3489+
stickDuration: number,
3490+
threadId: string | null,
3491+
eventType: K,
3492+
content: TimelineEvents[K] & { sticky_key: string },
3493+
txnId?: string,
3494+
): Promise<ISendEventResponse> {
3495+
// if (!(await this.doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS))) {
3496+
// throw new UnsupportedStickyEventsEndpointError(
3497+
// "Server does not support the sticky events",
3498+
// "sendStickyEvent",
3499+
// );
3500+
// }
3501+
3502+
this.addThreadRelationIfNeeded(content, threadId, roomId);
3503+
return this.sendCompleteEvent({
3504+
roomId,
3505+
threadId,
3506+
eventObject: { type: eventType, content },
3507+
queryDict: { msc4354_stick_duration_ms: stickDuration },
3508+
txnId,
3509+
});
3510+
}
3511+
34243512
/**
34253513
* Get all pending delayed events for the calling user.
34263514
*

src/errors.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class ClientStoppedError extends Error {
5454
}
5555

5656
/**
57-
* This error is thrown when the Homeserver does not support the delayed events enpdpoints.
57+
* This error is thrown when the Homeserver does not support the delayed events endpoints.
5858
*/
5959
export class UnsupportedDelayedEventsEndpointError extends Error {
6060
public constructor(
@@ -65,3 +65,16 @@ export class UnsupportedDelayedEventsEndpointError extends Error {
6565
this.name = "UnsupportedDelayedEventsEndpointError";
6666
}
6767
}
68+
69+
/**
70+
* This error is thrown when the Homeserver does not support the sticky events endpoints.
71+
*/
72+
export class UnsupportedStickyEventsEndpointError extends Error {
73+
public constructor(
74+
message: string,
75+
public clientEndpoint: "sendStickyEvent" | "sendStickyStateEvent",
76+
) {
77+
super(message);
78+
this.name = "UnsupportedStickyEventsEndpointError";
79+
}
80+
}

src/models/event.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface IEvent {
9696
membership?: Membership;
9797
unsigned: IUnsigned;
9898
redacts?: string;
99+
sticky?: number;
99100
}
100101

101102
export interface IAggregatedRelation {

0 commit comments

Comments
 (0)