Skip to content

Commit eeff216

Browse files
committed
feat: Support multipart message replies [WPB-15705]
1 parent 5e51801 commit eeff216

File tree

5 files changed

+176
-12
lines changed

5 files changed

+176
-12
lines changed

src/script/message/MessageHasher.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ const getTimestampBytes = (event: any): number[] => {
6161

6262
const getTextBytes = (event: any): number[] => utf8ToUtf16BE(event.data.content);
6363

64+
const getMultipartTextBytes = (event: any): number[] => utf8ToUtf16BE(event.data.text.content);
65+
6466
/**
6567
* Creates a hash of the given event.
6668
*
@@ -74,6 +76,10 @@ const hashEvent = (event: any): Promise<ArrayBuffer> => {
7476
specificBytes = getTextBytes(event);
7577
break;
7678
}
79+
case ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD: {
80+
specificBytes = getMultipartTextBytes(event);
81+
break;
82+
}
7783
case ClientEvent.CONVERSATION.LOCATION: {
7884
specificBytes = getLocationBytes(event);
7985
break;

src/script/repositories/conversation/EventBuilder/EventBuilder.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,29 @@ export type MultipartMessageAddEvent = ConversationEvent<
163163
CONVERSATION.MULTIPART_MESSAGE_ADD,
164164
{
165165
attachments: MultiPartContent['attachments'];
166-
text: MultiPartContent['text'] & {mentions?: string[]; previews?: string[]};
166+
text: MultiPartContent['text'] & {
167+
mentions?: string[];
168+
previews?: string[];
169+
quote?:
170+
| string
171+
| {
172+
message_id: string;
173+
user_id: string;
174+
hash: Uint8Array;
175+
}
176+
| {error: {type: string}};
177+
};
178+
replacing_message_id?: string;
167179
}
168-
>;
180+
> & {
181+
/** who have received/read the event */
182+
read_receipts?: ReadReceipt[];
183+
/** who reacted to the event */
184+
reactions?: UserReactionMap | ReactionMap;
185+
edited_time?: string;
186+
status: StatusType;
187+
version?: number;
188+
};
169189
export type MissedEvent = BaseEvent & {id: string; type: CONVERSATION.MISSED_MESSAGES};
170190
export type JoinedAfterMLSMigrationFinalisationEvent = BaseEvent & {
171191
type: CONVERSATION.JOINED_AFTER_MLS_MIGRATION;

src/script/repositories/event/EventService.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,20 @@ export class EventService {
203203
.table(StorageSchemata.OBJECT_STORE.EVENTS)
204204
.where(['conversation', 'time'])
205205
.between([conversationId, quotedMessageTime], [conversationId, new Date().toISOString()], true, true)
206-
.filter(event => event.data && event.data.quote && event.data.quote.message_id === quotedMessageId)
206+
.filter(event => {
207+
if (!event.data) {
208+
return false;
209+
}
210+
// Check normal message quote
211+
if (event.data.quote && event.data.quote.message_id === quotedMessageId) {
212+
return true;
213+
}
214+
// Check multipart message quote
215+
if (event.data.text?.quote && event.data.text.quote.message_id === quotedMessageId) {
216+
return true;
217+
}
218+
return false;
219+
})
207220
.toArray();
208221
return events;
209222
}
@@ -217,7 +230,20 @@ export class EventService {
217230
record.time <= new Date().toISOString()
218231
);
219232
})
220-
.filter(event => !!event.data && !!event.data.quote && event.data.quote.message_id === quotedMessageId)
233+
.filter(event => {
234+
if (!event.data) {
235+
return false;
236+
}
237+
// Check normal message quote
238+
if (event.data.quote && event.data.quote.message_id === quotedMessageId) {
239+
return true;
240+
}
241+
// Check multipart message quote
242+
if (event.data.text?.quote && event.data.text.quote.message_id === quotedMessageId) {
243+
return true;
244+
}
245+
return false;
246+
})
221247
.sort(compareEventsByConversation);
222248
}
223249

src/script/repositories/event/preprocessor/QuoteDecoderMiddleware.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import {Quote} from '@wireapp/protocol-messaging';
2121

22-
import {MessageAddEvent} from 'Repositories/conversation/EventBuilder';
22+
import {MessageAddEvent, MultipartMessageAddEvent} from 'Repositories/conversation/EventBuilder';
2323
import {StoredEvent} from 'Repositories/storage/record/EventRecord';
2424
import {getLogger, Logger} from 'Util/Logger';
2525
import {base64ToArray} from 'Util/util';
@@ -49,6 +49,12 @@ export class QuotedMessageMiddleware implements EventMiddleware {
4949
const originalMessageId = event.data.replacing_message_id;
5050
return originalMessageId ? this.handleEditEvent(event, originalMessageId) : this.handleAddEvent(event);
5151
}
52+
case ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD: {
53+
const originalMessageId = event.data.replacing_message_id;
54+
return originalMessageId
55+
? this.handleMultipartEditEvent(event, originalMessageId)
56+
: this.handleMultipartAddEvent(event);
57+
}
5258
}
5359
return event;
5460
}
@@ -100,4 +106,60 @@ export class QuotedMessageMiddleware implements EventMiddleware {
100106
const decoratedData = {...event.data, quote: quoteData};
101107
return {...event, data: decoratedData};
102108
}
109+
110+
private async handleMultipartEditEvent(
111+
event: MultipartMessageAddEvent,
112+
originalMessageId: string,
113+
): Promise<MultipartMessageAddEvent> {
114+
const originalEvent = (await this.eventService.loadEvent(event.conversation, originalMessageId)) as StoredEvent<
115+
MessageAddEvent | MultipartMessageAddEvent | undefined
116+
>;
117+
if (!originalEvent) {
118+
return event;
119+
}
120+
121+
const originalQuote =
122+
originalEvent.type === ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD
123+
? originalEvent.data.text.quote
124+
: originalEvent.data.quote;
125+
126+
const decoratedData = {...event.data, text: {...event.data.text, quote: originalQuote} as any};
127+
return {...event, data: decoratedData};
128+
}
129+
130+
private async handleMultipartAddEvent(event: MultipartMessageAddEvent): Promise<MultipartMessageAddEvent> {
131+
const rawQuote = event.data.text.quote;
132+
133+
if (!rawQuote || typeof rawQuote !== 'string') {
134+
return event;
135+
}
136+
137+
const encodedQuote = base64ToArray(rawQuote);
138+
const quote = Quote.decode(encodedQuote);
139+
this.logger.info(`Found quoted message in multipart: ${quote.quotedMessageId}`);
140+
141+
const messageId = quote.quotedMessageId;
142+
143+
const quotedMessage = await this.eventService.loadEvent(event.conversation, messageId);
144+
if (!quotedMessage) {
145+
this.logger.warn(`Quoted message with ID "${messageId}" not found.`);
146+
const quoteData = {
147+
error: {
148+
type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND,
149+
},
150+
};
151+
152+
const decoratedData = {...event.data, text: {...event.data.text, quote: quoteData} as any};
153+
return {...event, data: decoratedData};
154+
}
155+
156+
const quoteData = {
157+
message_id: messageId,
158+
user_id: quotedMessage.from,
159+
hash: quote.quotedMessageSha256,
160+
};
161+
162+
const decoratedData = {...event.data, text: {...event.data.text, quote: quoteData} as any};
163+
return {...event, data: decoratedData};
164+
}
103165
}

src/script/repositories/event/preprocessor/RepliesUpdaterMiddleware.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*
1818
*/
1919

20-
import {DeleteEvent, MessageAddEvent} from 'Repositories/conversation/EventBuilder';
20+
import {DeleteEvent, MessageAddEvent, MultipartMessageAddEvent} from 'Repositories/conversation/EventBuilder';
2121
import {StoredEvent} from 'Repositories/storage/record/EventRecord';
2222
import {getLogger, Logger} from 'Util/Logger';
2323

@@ -45,6 +45,11 @@ export class RepliesUpdaterMiddleware implements EventMiddleware {
4545
return originalMessageId ? this.handleEditEvent(event, originalMessageId) : event;
4646
}
4747

48+
case ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD: {
49+
const originalMessageId = event.data.replacing_message_id;
50+
return originalMessageId ? this.handleMultipartEditEvent(event, originalMessageId) : event;
51+
}
52+
4853
case ClientEvent.CONVERSATION.MESSAGE_DELETE: {
4954
return this.handleDeleteEvent(event);
5055
}
@@ -60,7 +65,11 @@ export class RepliesUpdaterMiddleware implements EventMiddleware {
6065
const {replies} = await this.findRepliesToMessage(event.conversation, originalMessageId);
6166
this.logger.info(`Invalidating '${replies.length}' replies to deleted message '${originalMessageId}'`);
6267
replies.forEach(async reply => {
63-
reply.data.quote = {error: {type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND}};
68+
if (reply.type === ClientEvent.CONVERSATION.MESSAGE_ADD) {
69+
reply.data.quote = {error: {type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND}};
70+
} else if (reply.type === ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD && reply.data.text) {
71+
reply.data.text.quote = {error: {type: QuoteEntity.ERROR.MESSAGE_NOT_FOUND}} as any;
72+
}
6473
await this.eventService.replaceEvent(reply);
6574
});
6675
return event;
@@ -77,9 +86,43 @@ export class RepliesUpdaterMiddleware implements EventMiddleware {
7786

7887
this.logger.info(`Updating '${replies.length}' replies to updated message '${originalMessageId}'`);
7988
replies.forEach(async reply => {
80-
const quote = reply.data.quote;
81-
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
82-
quote.message_id = event.id as string;
89+
if (reply.type === ClientEvent.CONVERSATION.MESSAGE_ADD) {
90+
const quote = reply.data.quote;
91+
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
92+
quote.message_id = event.id as string;
93+
}
94+
} else if (reply.type === ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD && reply.data.text?.quote) {
95+
const quote = reply.data.text.quote;
96+
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
97+
quote.message_id = event.id as string;
98+
}
99+
}
100+
await this.eventService.replaceEvent(reply);
101+
});
102+
return event;
103+
}
104+
105+
/**
106+
* will update the message ID of all the replies to an edited multipart message
107+
*/
108+
private async handleMultipartEditEvent(event: MultipartMessageAddEvent, originalMessageId: string) {
109+
const {originalEvent, replies} = await this.findRepliesToMessage(event.conversation, originalMessageId, event.id);
110+
if (!originalEvent) {
111+
return event;
112+
}
113+
114+
this.logger.info(`Updating '${replies.length}' replies to updated multipart message '${originalMessageId}'`);
115+
replies.forEach(async reply => {
116+
if (reply.type === ClientEvent.CONVERSATION.MESSAGE_ADD) {
117+
const quote = reply.data.quote;
118+
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
119+
quote.message_id = event.id as string;
120+
}
121+
} else if (reply.type === ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD && reply.data.text?.quote) {
122+
const quote = reply.data.text.quote;
123+
if (quote && typeof quote !== 'string' && 'message_id' in quote && 'id' in event) {
124+
quote.message_id = event.id as string;
125+
}
83126
}
84127
await this.eventService.replaceEvent(reply);
85128
});
@@ -91,10 +134,17 @@ export class RepliesUpdaterMiddleware implements EventMiddleware {
91134
messageId: string,
92135
/** in case the message was edited, we need to query the DB using the old event ID */
93136
previousMessageId?: string,
94-
): Promise<{originalEvent?: MessageAddEvent; replies: StoredEvent<MessageAddEvent>[]}> {
137+
): Promise<{
138+
originalEvent?: MessageAddEvent | MultipartMessageAddEvent;
139+
replies: StoredEvent<MessageAddEvent | MultipartMessageAddEvent>[];
140+
}> {
95141
const originalEvent = await this.eventService.loadEvent(conversationId, previousMessageId ?? messageId);
96142

97-
if (!originalEvent || originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD) {
143+
if (
144+
!originalEvent ||
145+
(originalEvent.type !== ClientEvent.CONVERSATION.MESSAGE_ADD &&
146+
originalEvent.type !== ClientEvent.CONVERSATION.MULTIPART_MESSAGE_ADD)
147+
) {
98148
return {
99149
replies: [],
100150
};

0 commit comments

Comments
 (0)