@massalabs/gossip-sdk 0.0.2-dev.20260317085237 → 0.0.2-dev.20260318110914

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/db/db.d.ts CHANGED
@@ -45,6 +45,9 @@ export interface Message {
45
45
  editOf?: {
46
46
  originalMsgId: Uint8Array;
47
47
  };
48
+ reactionOf?: {
49
+ originalMsgId: Uint8Array;
50
+ };
48
51
  }
49
52
  export interface UserProfile {
50
53
  userId: string;
@@ -96,7 +99,8 @@ export declare enum MessageType {
96
99
  FILE = "file",
97
100
  AUDIO = "audio",
98
101
  VIDEO = "video",
99
- DELETED = "deleted"
102
+ DELETED = "deleted",
103
+ REACTION = "reaction"
100
104
  }
101
105
  export interface ReadyAnnouncement {
102
106
  announcement_bytes: Uint8Array;
package/dist/db/db.js CHANGED
@@ -37,6 +37,7 @@ export var MessageType;
37
37
  MessageType["AUDIO"] = "audio";
38
38
  MessageType["VIDEO"] = "video";
39
39
  MessageType["DELETED"] = "deleted";
40
+ MessageType["REACTION"] = "reaction";
40
41
  })(MessageType || (MessageType = {}));
41
42
  /** Serialize a SendAnnouncement to a JSON string for SQLite text column */
42
43
  export function serializeSendAnnouncement(announcement) {
@@ -48,4 +48,10 @@ export const MIGRATIONS = [
48
48
  'ALTER TABLE `discussions` ADD COLUMN `pinned` integer NOT NULL DEFAULT 0;',
49
49
  ],
50
50
  },
51
+ {
52
+ idx: 3,
53
+ tag: '0003_messages_reaction_of',
54
+ when: 1742000000000,
55
+ statements: ['ALTER TABLE `messages` ADD COLUMN `reactionOf` text;'],
56
+ },
51
57
  ];
@@ -9,6 +9,7 @@ export declare class MessageQueries {
9
9
  getById(id: number): Promise<MessageRow | undefined>;
10
10
  getByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
11
11
  getVisibleByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
12
+ getReactionsByOwnerAndContact(ownerUserId: string, contactUserId: string): Promise<MessageRow[]>;
12
13
  getByOwnerAndSeeker(ownerUserId: string, seeker: Uint8Array): Promise<MessageRow | undefined>;
13
14
  findByMessageId(ownerUserId: string, contactUserId: string, messageId: Uint8Array): Promise<MessageRow | undefined>;
14
15
  insert(values: MessageInsert): Promise<number>;
@@ -32,6 +32,8 @@ export class MessageQueries {
32
32
  .where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId),
33
33
  // Hide keep-alive messages from UI
34
34
  ne(schema.messages.type, MessageType.KEEP_ALIVE),
35
+ // Hide reaction messages (and any deleted reaction rows) from main message list
36
+ ne(schema.messages.type, MessageType.REACTION), sql `reactionOf IS NULL`,
35
37
  // Hide delete control messages (outgoing DELETED with empty content)
36
38
  or(ne(schema.messages.type, MessageType.DELETED), ne(schema.messages.direction, MessageDirection.OUTGOING), ne(schema.messages.content, '')),
37
39
  // Hide edit control messages tagged via metadata.control === 'edit'
@@ -39,6 +41,14 @@ export class MessageQueries {
39
41
  .orderBy(asc(schema.messages.id))
40
42
  .all();
41
43
  }
44
+ async getReactionsByOwnerAndContact(ownerUserId, contactUserId) {
45
+ return this.conn.db
46
+ .select()
47
+ .from(schema.messages)
48
+ .where(and(eq(schema.messages.ownerUserId, ownerUserId), eq(schema.messages.contactUserId, contactUserId), eq(schema.messages.type, MessageType.REACTION)))
49
+ .orderBy(asc(schema.messages.timestamp), asc(schema.messages.id))
50
+ .all();
51
+ }
42
52
  async getByOwnerAndSeeker(ownerUserId, seeker) {
43
53
  return this.conn.db
44
54
  .select()
@@ -306,6 +306,25 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
306
306
  }, {}, {
307
307
  length: number | undefined;
308
308
  }>;
309
+ reactionOf: import("drizzle-orm/sqlite-core").SQLiteColumn<{
310
+ name: "reactionOf";
311
+ tableName: "messages";
312
+ dataType: "string";
313
+ columnType: "SQLiteText";
314
+ data: string;
315
+ driverParam: string;
316
+ notNull: false;
317
+ hasDefault: false;
318
+ isPrimaryKey: false;
319
+ isAutoincrement: false;
320
+ hasRuntimeDefault: false;
321
+ enumValues: [string, ...string[]];
322
+ baseColumn: never;
323
+ identity: undefined;
324
+ generated: undefined;
325
+ }, {}, {
326
+ length: number | undefined;
327
+ }>;
309
328
  encryptedMessage: import("drizzle-orm/sqlite-core").SQLiteColumn<{
310
329
  name: "encryptedMessage";
311
330
  tableName: "messages";
@@ -17,6 +17,7 @@ export const messages = sqliteTable('messages', {
17
17
  forwardOf: text('forwardOf'),
18
18
  deleteOf: text('deleteOf'),
19
19
  editOf: text('editOf'),
20
+ reactionOf: text('reactionOf'),
20
21
  encryptedMessage: bytes('encryptedMessage'),
21
22
  whenToSend: integer('whenToSend', { mode: 'timestamp_ms' }),
22
23
  }, table => [
@@ -4,7 +4,8 @@ export declare enum MessageType {
4
4
  MESSAGE_TYPE_FORWARD = 2,
5
5
  MESSAGE_TYPE_KEEP_ALIVE = 3,
6
6
  MESSAGE_TYPE_DELETE = 4,
7
- MESSAGE_TYPE_EDIT = 5
7
+ MESSAGE_TYPE_EDIT = 5,
8
+ MESSAGE_TYPE_REACTION = 6
8
9
  }
9
10
  export interface Message {
10
11
  messageType?: MessageType;
@@ -8,6 +8,7 @@ export var MessageType;
8
8
  MessageType[MessageType["MESSAGE_TYPE_KEEP_ALIVE"] = 3] = "MESSAGE_TYPE_KEEP_ALIVE";
9
9
  MessageType[MessageType["MESSAGE_TYPE_DELETE"] = 4] = "MESSAGE_TYPE_DELETE";
10
10
  MessageType[MessageType["MESSAGE_TYPE_EDIT"] = 5] = "MESSAGE_TYPE_EDIT";
11
+ MessageType[MessageType["MESSAGE_TYPE_REACTION"] = 6] = "MESSAGE_TYPE_REACTION";
11
12
  })(MessageType || (MessageType = {}));
12
13
  const textEncoder = new TextEncoder();
13
14
  const textDecoder = new TextDecoder();
@@ -87,6 +87,8 @@ export declare class MessageService {
87
87
  getMessages(contactUserId: string): Promise<Message[]>;
88
88
  /** Get only user-visible messages for a contact (filtered + ordered). */
89
89
  getVisibleMessages(contactUserId: string): Promise<Message[]>;
90
+ /** Get all non-deleted reaction messages for a contact. */
91
+ getReactions(contactUserId: string): Promise<Message[]>;
90
92
  /** Send a message, queued via QueueManager if available */
91
93
  send(message: Omit<Message, 'id'>): Promise<SendMessageResult>;
92
94
  /**
@@ -102,6 +104,7 @@ export declare class MessageService {
102
104
  * so the peer can mark their copy as deleted as well.
103
105
  */
104
106
  deleteMessage(id: number): Promise<boolean>;
107
+ sendReaction(contactUserId: string, emoji: string, originalMsgId: Uint8Array): Promise<SendMessageResult>;
105
108
  /**
106
109
  * Edit an outgoing message by its database ID.
107
110
  * Updates the local content (preserving timestamp) and enqueues an edit
@@ -7,7 +7,7 @@
7
7
  import { MessageDirection, MessageStatus, MessageType, MESSAGE_ID_SIZE, } from '../db/index.js';
8
8
  import { decodeUserId, encodeUserId } from '../utils/userId.js';
9
9
  import { SessionStatus } from '../wasm/bindings.js';
10
- import { serializeRegularMessage, serializeReplyMessage, serializeForwardMessage, serializeKeepAliveMessage, serializeDeleteMessage, serializeEditMessage, deserializeMessage, } from '../utils/messageSerialization.js';
10
+ import { serializeRegularMessage, serializeReplyMessage, serializeForwardMessage, serializeKeepAliveMessage, serializeDeleteMessage, serializeEditMessage, serializeReactionMessage, deserializeMessage, } from '../utils/messageSerialization.js';
11
11
  import { encodeToBase64, decodeFromBase64 } from '../utils/base64.js';
12
12
  import { sessionStatusToString } from '../wasm/session.js';
13
13
  import { Logger } from '../utils/logs.js';
@@ -89,6 +89,21 @@ function deserializeEditOf(json) {
89
89
  originalMsgId: decodeFromBase64(parsed.originalMsgId),
90
90
  };
91
91
  }
92
+ function serializeReactionOf(reactionOf) {
93
+ if (!reactionOf)
94
+ return null;
95
+ return JSON.stringify({
96
+ originalMsgId: encodeToBase64(reactionOf.originalMsgId),
97
+ });
98
+ }
99
+ function deserializeReactionOf(json) {
100
+ if (!json)
101
+ return undefined;
102
+ const parsed = JSON.parse(json);
103
+ return {
104
+ originalMsgId: decodeFromBase64(parsed.originalMsgId),
105
+ };
106
+ }
92
107
  /** Serialize metadata to JSON string. */
93
108
  function serializeMetadata(metadata) {
94
109
  if (!metadata)
@@ -120,6 +135,7 @@ export function rowToMessage(row) {
120
135
  forwardOf: deserializeForwardOf(row.forwardOf),
121
136
  deleteOf: deserializeDeleteOf(row.deleteOf ?? null),
122
137
  editOf: deserializeEditOf(row.editOf ?? null),
138
+ reactionOf: deserializeReactionOf(row.reactionOf ?? null),
123
139
  encryptedMessage: row.encryptedMessage ?? undefined,
124
140
  whenToSend: row.whenToSend ?? undefined,
125
141
  };
@@ -293,11 +309,14 @@ export class MessageService {
293
309
  forwardOf: serializeForwardOf(message.forwardOf),
294
310
  deleteOf: serializeDeleteOf(message.deleteOf),
295
311
  editOf: serializeEditOf(message.editOf),
312
+ reactionOf: serializeReactionOf(message.reactionOf),
296
313
  encryptedMessage: message.encryptedMessage,
297
314
  whenToSend: message.whenToSend,
298
315
  });
299
316
  const discussion = await this.queries.discussions.getByOwnerAndContact(message.ownerUserId, message.contactUserId);
300
- if (discussion && message.type !== MessageType.KEEP_ALIVE) {
317
+ if (discussion &&
318
+ message.type !== MessageType.KEEP_ALIVE &&
319
+ message.type !== MessageType.REACTION) {
301
320
  await this.queries.discussions.updateById(discussion.id, {
302
321
  lastMessageId: messageId,
303
322
  lastMessageContent: message.content,
@@ -345,6 +364,7 @@ export class MessageService {
345
364
  forwardOf: deserialized.forwardOf,
346
365
  deleteOf: deserialized.deleteOf,
347
366
  editOf: deserialized.editOf,
367
+ reactionOf: deserialized.reactionOf,
348
368
  });
349
369
  }
350
370
  catch (deserializationError) {
@@ -406,6 +426,33 @@ export class MessageService {
406
426
  // Do not insert a new message row for edit control messages
407
427
  continue;
408
428
  }
429
+ // Handle reaction messages by inserting a separate row
430
+ if (message.type === MessageType.REACTION &&
431
+ message.reactionOf?.originalMsgId) {
432
+ const discussion = await this.queries.discussions.getByOwnerAndContact(ownerUserId, message.senderId);
433
+ if (!discussion) {
434
+ log.error('no discussion for incoming reaction message', {
435
+ senderId: message.senderId,
436
+ preview: message.content.slice(0, 16),
437
+ });
438
+ continue;
439
+ }
440
+ const id = await this.queries.messages.insert({
441
+ messageId: message.messageId,
442
+ ownerUserId,
443
+ contactUserId: discussion.contactUserId,
444
+ content: message.content,
445
+ type: MessageType.REACTION,
446
+ direction: MessageDirection.INCOMING,
447
+ status: MessageStatus.DELIVERED,
448
+ timestamp: message.sentAt,
449
+ metadata: serializeMetadata({}),
450
+ reactionOf: serializeReactionOf(message.reactionOf),
451
+ });
452
+ storedIds.push(id);
453
+ // Do not update discussion lastMessageContent for reactions
454
+ continue;
455
+ }
409
456
  const discussion = await this.queries.discussions.getByOwnerAndContact(ownerUserId, message.senderId);
410
457
  if (!discussion) {
411
458
  log.error('no discussion for incoming message', {
@@ -631,6 +678,19 @@ export class MessageService {
631
678
  };
632
679
  }
633
680
  }
681
+ else if (message.type === MessageType.REACTION && message.reactionOf) {
682
+ const originalMsgId = message.reactionOf.originalMsgId;
683
+ if (!originalMsgId || originalMsgId.length !== MESSAGE_ID_SIZE) {
684
+ return {
685
+ success: false,
686
+ error: 'Original messageId is required for reaction messages',
687
+ };
688
+ }
689
+ return {
690
+ success: true,
691
+ data: serializeReactionMessage(message.content, originalMsgId, message.messageId),
692
+ };
693
+ }
634
694
  else {
635
695
  // Regular message with type tag
636
696
  return {
@@ -896,6 +956,11 @@ export class MessageService {
896
956
  const rows = await this.queries.messages.getVisibleByOwnerAndContact(this.session.userIdEncoded, contactUserId);
897
957
  return rows.map(rowToMessage);
898
958
  }
959
+ /** Get all non-deleted reaction messages for a contact. */
960
+ async getReactions(contactUserId) {
961
+ const rows = await this.queries.messages.getReactionsByOwnerAndContact(this.session.userIdEncoded, contactUserId);
962
+ return rows.map(rowToMessage);
963
+ }
899
964
  /** Send a message, queued via QueueManager if available */
900
965
  async send(message) {
901
966
  if (this.queueManager) {
@@ -970,6 +1035,21 @@ export class MessageService {
970
1035
  await this.refreshService?.stateUpdate();
971
1036
  return true;
972
1037
  }
1038
+ async sendReaction(contactUserId, emoji, originalMsgId) {
1039
+ const message = {
1040
+ ownerUserId: this.session.userIdEncoded,
1041
+ contactUserId,
1042
+ content: emoji,
1043
+ type: MessageType.REACTION,
1044
+ direction: MessageDirection.OUTGOING,
1045
+ status: MessageStatus.WAITING_SESSION,
1046
+ timestamp: new Date(),
1047
+ reactionOf: { originalMsgId },
1048
+ };
1049
+ const result = await this.send(message);
1050
+ await this.refreshService?.stateUpdate();
1051
+ return result;
1052
+ }
973
1053
  /**
974
1054
  * Edit an outgoing message by its database ID.
975
1055
  * Updates the local content (preserving timestamp) and enqueues an edit
@@ -25,6 +25,9 @@ export interface DeserializedMessage {
25
25
  editOf?: {
26
26
  originalMsgId: Uint8Array;
27
27
  };
28
+ reactionOf?: {
29
+ originalMsgId: Uint8Array;
30
+ };
28
31
  type: MessageType;
29
32
  }
30
33
  /**
@@ -87,6 +90,7 @@ export declare function serializeDeleteMessage(originalMsgId: Uint8Array, messag
87
90
  * @returns Serialized edit message bytes
88
91
  */
89
92
  export declare function serializeEditMessage(newContent: string, originalMsgId: Uint8Array, messageId: Uint8Array): Uint8Array;
93
+ export declare function serializeReactionMessage(emoji: string, originalMsgId: Uint8Array, messageId: Uint8Array): Uint8Array;
90
94
  /**
91
95
  * Deserialize a message from bytes
92
96
  *
@@ -136,6 +136,20 @@ export function serializeEditMessage(newContent, originalMsgId, messageId) {
136
136
  citedMsgId: originalMsgId,
137
137
  });
138
138
  }
139
+ export function serializeReactionMessage(emoji, originalMsgId, messageId) {
140
+ if (messageId.length !== MESSAGE_ID_SIZE) {
141
+ throw new Error(`messageId must be ${MESSAGE_ID_SIZE} bytes`);
142
+ }
143
+ if (originalMsgId.length !== MESSAGE_ID_SIZE) {
144
+ throw new Error(`originalMsgId must be ${MESSAGE_ID_SIZE} bytes`);
145
+ }
146
+ return ProtoMessage.encode({
147
+ messageType: ProtoMessageType.MESSAGE_TYPE_REACTION,
148
+ messageId,
149
+ content: emoji,
150
+ citedMsgId: originalMsgId,
151
+ });
152
+ }
139
153
  /**
140
154
  * Deserialize a message from bytes
141
155
  *
@@ -161,6 +175,7 @@ export function deserializeMessage(buffer) {
161
175
  let replyTo = undefined;
162
176
  let deleteOf = undefined;
163
177
  let editOf = undefined;
178
+ let reactionOf = undefined;
164
179
  if (protoType === ProtoMessageType.MESSAGE_TYPE_REPLY) {
165
180
  if (citedMsgId && citedMsgId.length === MESSAGE_ID_SIZE) {
166
181
  replyTo = {
@@ -191,6 +206,16 @@ export function deserializeMessage(buffer) {
191
206
  throw new Error(`invalid message format: message of type edit but citedMsgId empty or not correct size (${MESSAGE_ID_SIZE})`);
192
207
  }
193
208
  }
209
+ if (protoType === ProtoMessageType.MESSAGE_TYPE_REACTION) {
210
+ if (citedMsgId && citedMsgId.length === MESSAGE_ID_SIZE) {
211
+ reactionOf = {
212
+ originalMsgId: citedMsgId,
213
+ };
214
+ }
215
+ else {
216
+ throw new Error(`invalid message format: message of type reaction but citedMsgId empty or not correct size (${MESSAGE_ID_SIZE})`);
217
+ }
218
+ }
194
219
  let forwardOf = undefined;
195
220
  if (protoType === ProtoMessageType.MESSAGE_TYPE_FORWARD) {
196
221
  if (decoded.forwardedContent &&
@@ -212,8 +237,11 @@ export function deserializeMessage(buffer) {
212
237
  forwardOf,
213
238
  deleteOf,
214
239
  editOf,
240
+ reactionOf,
215
241
  type: protoType === ProtoMessageType.MESSAGE_TYPE_DELETE
216
242
  ? MessageType.DELETED
217
- : MessageType.TEXT,
243
+ : protoType === ProtoMessageType.MESSAGE_TYPE_REACTION
244
+ ? MessageType.REACTION
245
+ : MessageType.TEXT,
218
246
  };
219
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massalabs/gossip-sdk",
3
- "version": "0.0.2-dev.20260317085237",
3
+ "version": "0.0.2-dev.20260318110914",
4
4
  "description": "Gossip SDK for automation, chatbot, and integration use cases",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",