-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathindex.ts
207 lines (189 loc) · 6.57 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import {
type MatrixEvent,
type RoomMember,
type MatrixClient,
type IEvent,
} from 'matrix-js-sdk';
import { RoomState } from '@cardstack/host/lib/matrix-classes/room';
import type * as CardAPI from 'https://cardstack.com/base/card-api';
import type {
CommandEvent,
CommandResultEvent,
MatrixEvent as DiscreteMatrixEvent,
ReactionEvent,
} from 'https://cardstack.com/base/matrix-event';
import type * as MatrixSDK from 'matrix-js-sdk';
export * as Membership from './membership';
export * as Timeline from './timeline';
export interface RoomEvent extends RoomMeta {
eventId: string;
roomId: string;
timestamp: number;
}
export interface RoomInvite extends RoomEvent {
sender: string;
}
export interface RoomMeta {
name?: string;
}
export type Event = Partial<IEvent> & {
status: MatrixSDK.EventStatus | null;
error?: MatrixSDK.MatrixError;
};
export interface EventSendingContext {
setRoom: (roomId: string, room: RoomState) => void;
// Note: Notice our implementation looks completely synchronous but bcos of the way we process our matrix event as a subscriber. getRoom is inherently asynchronous
// getRoom is async because subscribe handlers should be synchronous and we should handle asynchrony outside of the handler code, otherwise, handler/queues will become confused
// If you look around the codebase, you will see instances of await getRoom which is the correct pattern to use although the types do not reflect so
// The reason why the types are locked in as synchronous is because we don't have a good way to react or access .events which hides behind this promise
// If we await getRoom before accessing .events, we lose trackedness
// TODO: Resolve matrix async types with this https://linear.app/cardstack/issue/CS-6987/get-room-resource-to-register-with-matrix-event-handler
getRoom: (roomId: string) => RoomState | undefined;
cardAPI: typeof CardAPI;
}
export interface Context extends EventSendingContext {
flushTimeline: Promise<void> | undefined;
flushMembership: Promise<void> | undefined;
roomMembershipQueue: { event: MatrixEvent; member: RoomMember }[];
timelineQueue: { event: MatrixEvent; oldEventId?: string }[];
client: MatrixClient | undefined;
matrixSDK: typeof MatrixSDK | undefined;
handleMessage?: (
context: Context,
event: Event,
roomId: string,
) => Promise<void>;
addEventReadReceipt(eventId: string, receipt: { readAt: Date }): void;
}
export async function addRoomEvent(context: EventSendingContext, event: Event) {
let { event_id: eventId, room_id: roomId, state_key: stateKey } = event;
// If we are receiving an event which contains
// a data field, we need to parse it
// because matrix doesn't support all json types
// Corresponding encoding is done in
// sendEvent in the matrix-service
if (event.content?.data) {
if (typeof event.content.data !== 'string') {
console.warn(
`skipping matrix event ${
eventId ?? stateKey
}, event.content.data is not serialized properly`,
);
return;
}
event.content.data = JSON.parse(event.content.data);
}
eventId = eventId ?? stateKey; // room state may not necessary have an event ID
if (!eventId) {
throw new Error(
`bug: event ID is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
if (!roomId) {
throw new Error(
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
let room = context.getRoom(roomId);
if (!room) {
room = new RoomState();
context.setRoom(roomId, room);
}
// duplicate events may be emitted from matrix, as well as the resolved room card might already contain this event
if (!room.events.find((e) => e.event_id === eventId)) {
room.events = [
...(room.events ?? []),
event as unknown as DiscreteMatrixEvent,
];
}
}
export async function updateRoomEvent(
context: EventSendingContext,
event: Event,
oldEventId: string,
) {
if (event.content?.data && typeof event.content.data === 'string') {
event.content.data = JSON.parse(event.content.data);
}
let { event_id: eventId, room_id: roomId, state_key: stateKey } = event;
eventId = eventId ?? stateKey; // room state may not necessary have an event ID
if (!eventId) {
throw new Error(
`bug: event ID is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
if (!roomId) {
throw new Error(
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
let room = context.getRoom(roomId);
if (!room) {
throw new Error(
`bug: unknown room for event ${JSON.stringify(event, null, 2)}`,
);
}
let oldEventIndex = room.events.findIndex((e) => e.event_id === oldEventId);
if (oldEventIndex >= 0) {
room.events[oldEventIndex] = event as unknown as DiscreteMatrixEvent;
room.events = [...room.events];
}
}
export async function getRoomEvents(
context: EventSendingContext,
roomId: string,
): Promise<DiscreteMatrixEvent[]> {
if (!roomId) {
throw new Error(
`bug: roomId is undefined for event ${JSON.stringify(event, null, 2)}`,
);
}
let room = context.getRoom(roomId);
return room?.events ?? [];
}
export async function getCommandResultEvents(
context: EventSendingContext,
roomId: string,
): Promise<CommandResultEvent[]> {
let events = await getRoomEvents(context, roomId);
return events.filter((e) => isCommandResultEvent(e)) as CommandResultEvent[];
}
export async function getCommandReactionEvents(
context: EventSendingContext,
roomId: string,
): Promise<ReactionEvent[]> {
let events = await getRoomEvents(context, roomId);
return events.filter((e) =>
isCommandReactionStatusApplied(e),
) as ReactionEvent[];
}
export function isCommandEvent(
event: DiscreteMatrixEvent,
): event is CommandEvent {
return (
event.type === 'm.room.message' &&
typeof event.content === 'object' &&
event.content.msgtype === 'org.boxel.command' &&
event.content.format === 'org.matrix.custom.html' &&
typeof event.content.data === 'object' &&
typeof event.content.data.toolCall === 'object'
);
}
export const isCommandReactionStatusApplied = (
event: DiscreteMatrixEvent,
): event is ReactionEvent => {
return (
event.type === 'm.reaction' &&
event.content['m.relates_to']?.rel_type === 'm.annotation' &&
event.content['m.relates_to']?.key === 'applied'
);
};
export function isCommandResultEvent(
event: DiscreteMatrixEvent,
): event is CommandResultEvent {
return (
event.type === 'm.room.message' &&
typeof event.content === 'object' &&
event.content.msgtype === 'org.boxel.commandResult'
);
}