@vex-chat/libvex 6.6.4 → 6.8.0
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/Client.d.ts +11 -7
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +93 -81
- package/dist/Client.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/messageExtra.d.ts +114 -0
- package/dist/messageExtra.d.ts.map +1 -0
- package/dist/messageExtra.js +383 -0
- package/dist/messageExtra.js.map +1 -0
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/sqlite.d.ts +1 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +68 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +138 -112
- package/src/__tests__/harness/shared-suite.ts +38 -1
- package/src/__tests__/messageExtra.test.ts +97 -0
- package/src/index.ts +29 -0
- package/src/messageExtra.ts +585 -0
- package/src/storage/schema.ts +1 -0
- package/src/storage/sqlite.ts +82 -4
package/src/Client.ts
CHANGED
|
@@ -606,6 +606,8 @@ export interface Message {
|
|
|
606
606
|
decrypted: boolean;
|
|
607
607
|
/** Whether this message was received or sent by the current client. */
|
|
608
608
|
direction: "incoming" | "outgoing";
|
|
609
|
+
/** Optional encrypted client metadata attached to this message. */
|
|
610
|
+
extra?: null | string | undefined;
|
|
609
611
|
/** `true` when this message was forwarded to another owned device. */
|
|
610
612
|
forward: boolean;
|
|
611
613
|
/** Channel ID for group messages; `null` for direct messages. */
|
|
@@ -625,7 +627,7 @@ export interface Message {
|
|
|
625
627
|
* with {@link ClientOptions.localMessageRetentionDays} to pick the
|
|
626
628
|
* shorter local retention window. Ignored when absent.
|
|
627
629
|
*/
|
|
628
|
-
retentionHintDays?: number;
|
|
630
|
+
retentionHintDays?: number | undefined;
|
|
629
631
|
/** Sender device ID. */
|
|
630
632
|
sender: string;
|
|
631
633
|
/** Time the message was created/received. */
|
|
@@ -850,6 +852,7 @@ const messageSchema: z.ZodType<Message> = z.object({
|
|
|
850
852
|
authorID: z.string(),
|
|
851
853
|
decrypted: z.boolean(),
|
|
852
854
|
direction: z.enum(["incoming", "outgoing"]),
|
|
855
|
+
extra: z.string().nullable().optional(),
|
|
853
856
|
forward: z.boolean(),
|
|
854
857
|
group: z.string().nullable(),
|
|
855
858
|
mailID: z.string(),
|
|
@@ -857,10 +860,94 @@ const messageSchema: z.ZodType<Message> = z.object({
|
|
|
857
860
|
nonce: z.string(),
|
|
858
861
|
readerID: z.string(),
|
|
859
862
|
recipient: z.string(),
|
|
863
|
+
retentionHintDays: z.number().optional(),
|
|
860
864
|
sender: z.string(),
|
|
861
865
|
timestamp: z.string(),
|
|
862
866
|
});
|
|
863
867
|
|
|
868
|
+
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
869
|
+
|
|
870
|
+
interface DecodedMessagePlaintext {
|
|
871
|
+
extra?: null | string | undefined;
|
|
872
|
+
message: string;
|
|
873
|
+
retentionHintDays?: number | undefined;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function decodeMessageBlob(body: string): DecodedMessagePlaintext {
|
|
877
|
+
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
878
|
+
return { message: body };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
try {
|
|
882
|
+
const raw = JSON.parse(
|
|
883
|
+
body.slice(MESSAGE_BLOB_PREFIX.length),
|
|
884
|
+
) as unknown;
|
|
885
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
886
|
+
return { message: body };
|
|
887
|
+
}
|
|
888
|
+
const message = Reflect.get(raw, "message");
|
|
889
|
+
if (typeof message !== "string") {
|
|
890
|
+
return { message: body };
|
|
891
|
+
}
|
|
892
|
+
const extra = Reflect.get(raw, "extra");
|
|
893
|
+
return {
|
|
894
|
+
...(extra === null || typeof extra === "string" ? { extra } : {}),
|
|
895
|
+
message,
|
|
896
|
+
};
|
|
897
|
+
} catch {
|
|
898
|
+
return { message: body };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function decodeMessagePlaintext(plaintext: string): DecodedMessagePlaintext {
|
|
903
|
+
const stripped = stripVexRetentionEnvelope(plaintext);
|
|
904
|
+
const blob = decodeMessageBlob(stripped.body);
|
|
905
|
+
return stripped.retentionHintDays !== undefined
|
|
906
|
+
? {
|
|
907
|
+
...blob,
|
|
908
|
+
retentionHintDays: stripped.retentionHintDays,
|
|
909
|
+
}
|
|
910
|
+
: blob;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function encodeMessagePlaintext(
|
|
914
|
+
message: string,
|
|
915
|
+
opts?: MessageSendOptions,
|
|
916
|
+
): string {
|
|
917
|
+
const body =
|
|
918
|
+
opts?.extra === undefined
|
|
919
|
+
? message
|
|
920
|
+
: MESSAGE_BLOB_PREFIX +
|
|
921
|
+
JSON.stringify({
|
|
922
|
+
extra: opts.extra,
|
|
923
|
+
message,
|
|
924
|
+
});
|
|
925
|
+
return formatVexRetentionEnvelope(body, opts?.retentionHintDays);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function messageFromDecodedPlaintext(
|
|
929
|
+
decoded: DecodedMessagePlaintext,
|
|
930
|
+
): Pick<Message, "extra" | "message" | "retentionHintDays"> {
|
|
931
|
+
return {
|
|
932
|
+
...(decoded.extra !== undefined ? { extra: decoded.extra } : {}),
|
|
933
|
+
message: decoded.message,
|
|
934
|
+
...(decoded.retentionHintDays !== undefined
|
|
935
|
+
? { retentionHintDays: decoded.retentionHintDays }
|
|
936
|
+
: {}),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function normalizeForwardedMessage(message: Message): Message {
|
|
941
|
+
const decoded = decodeMessagePlaintext(message.message);
|
|
942
|
+
return {
|
|
943
|
+
...message,
|
|
944
|
+
...messageFromDecodedPlaintext({
|
|
945
|
+
...decoded,
|
|
946
|
+
extra: decoded.extra !== undefined ? decoded.extra : message.extra,
|
|
947
|
+
}),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
864
951
|
/** Zod schema for a single inbox entry from getMail: [header, mailBody, timestamp]. */
|
|
865
952
|
const mailInboxEntry = z.tuple([
|
|
866
953
|
z.custom<Uint8Array>((val) => val instanceof Uint8Array),
|
|
@@ -929,7 +1016,7 @@ export interface Messages {
|
|
|
929
1016
|
group: (
|
|
930
1017
|
channelID: string,
|
|
931
1018
|
message: string,
|
|
932
|
-
opts?:
|
|
1019
|
+
opts?: MessageSendOptions,
|
|
933
1020
|
) => Promise<void>;
|
|
934
1021
|
/** Deletes all locally stored message history. */
|
|
935
1022
|
purge: () => Promise<void>;
|
|
@@ -941,10 +1028,17 @@ export interface Messages {
|
|
|
941
1028
|
send: (
|
|
942
1029
|
userID: string,
|
|
943
1030
|
message: string,
|
|
944
|
-
opts?:
|
|
1031
|
+
opts?: MessageSendOptions,
|
|
945
1032
|
) => Promise<void>;
|
|
946
1033
|
}
|
|
947
1034
|
|
|
1035
|
+
export interface MessageSendOptions {
|
|
1036
|
+
/** Optional encrypted client metadata for message-level features. */
|
|
1037
|
+
extra?: null | string | undefined;
|
|
1038
|
+
/** Optional peer hint (1-30 days) for local history retention. */
|
|
1039
|
+
retentionHintDays?: number | undefined;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
948
1042
|
/**
|
|
949
1043
|
* @ignore
|
|
950
1044
|
*/
|
|
@@ -1275,7 +1369,7 @@ export class Client {
|
|
|
1275
1369
|
group: (
|
|
1276
1370
|
channelID: string,
|
|
1277
1371
|
message: string,
|
|
1278
|
-
opts?:
|
|
1372
|
+
opts?: MessageSendOptions,
|
|
1279
1373
|
) => this.sendGroupMessage(channelID, message, opts),
|
|
1280
1374
|
purge: this.purgeHistory.bind(this),
|
|
1281
1375
|
/**
|
|
@@ -1297,11 +1391,8 @@ export class Client {
|
|
|
1297
1391
|
* @param userID - The user to send a message to.
|
|
1298
1392
|
* @param message - The message to send.
|
|
1299
1393
|
*/
|
|
1300
|
-
send: (
|
|
1301
|
-
userID
|
|
1302
|
-
message: string,
|
|
1303
|
-
opts?: { retentionHintDays?: number },
|
|
1304
|
-
) => this.sendMessage(userID, message, opts),
|
|
1394
|
+
send: (userID: string, message: string, opts?: MessageSendOptions) =>
|
|
1395
|
+
this.sendMessage(userID, message, opts),
|
|
1305
1396
|
};
|
|
1306
1397
|
/**
|
|
1307
1398
|
* Server moderation helper methods.
|
|
@@ -2704,7 +2795,7 @@ export class Client {
|
|
|
2704
2795
|
: null;
|
|
2705
2796
|
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
2706
2797
|
const emitMsg: Message = forwardedMsg
|
|
2707
|
-
? { ...forwardedMsg, forward: true }
|
|
2798
|
+
? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
|
|
2708
2799
|
: {
|
|
2709
2800
|
authorID: mail.authorID,
|
|
2710
2801
|
decrypted: true,
|
|
@@ -2712,7 +2803,9 @@ export class Client {
|
|
|
2712
2803
|
forward: mail.forward,
|
|
2713
2804
|
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
2714
2805
|
mailID: mail.mailID,
|
|
2715
|
-
|
|
2806
|
+
...messageFromDecodedPlaintext(
|
|
2807
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(message)),
|
|
2808
|
+
),
|
|
2716
2809
|
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2717
2810
|
readerID: mail.readerID,
|
|
2718
2811
|
recipient: mail.recipient,
|
|
@@ -4098,39 +4191,19 @@ export class Client {
|
|
|
4098
4191
|
if (!mail.forward) {
|
|
4099
4192
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
4100
4193
|
}
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
const stripped =
|
|
4105
|
-
stripVexRetentionEnvelope(plaintext);
|
|
4106
|
-
incomingPlain = stripped.body;
|
|
4107
|
-
hintFromEnvelope = stripped.retentionHintDays;
|
|
4108
|
-
}
|
|
4194
|
+
const decodedPlaintext = mail.forward
|
|
4195
|
+
? null
|
|
4196
|
+
: decodeMessagePlaintext(plaintext);
|
|
4109
4197
|
|
|
4110
4198
|
// emit the message
|
|
4111
4199
|
const fwdMsg1 = mail.forward
|
|
4112
4200
|
? messageSchema.parse(msgpack.decode(unsealed))
|
|
4113
4201
|
: null;
|
|
4114
4202
|
const message: Message = fwdMsg1
|
|
4115
|
-
?
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
);
|
|
4120
|
-
const base: Message = {
|
|
4121
|
-
...fwdMsg1,
|
|
4122
|
-
forward: true,
|
|
4123
|
-
message: stripped.body,
|
|
4124
|
-
};
|
|
4125
|
-
return stripped.retentionHintDays !==
|
|
4126
|
-
undefined
|
|
4127
|
-
? {
|
|
4128
|
-
...base,
|
|
4129
|
-
retentionHintDays:
|
|
4130
|
-
stripped.retentionHintDays,
|
|
4131
|
-
}
|
|
4132
|
-
: base;
|
|
4133
|
-
})()
|
|
4203
|
+
? {
|
|
4204
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
4205
|
+
forward: true,
|
|
4206
|
+
}
|
|
4134
4207
|
: {
|
|
4135
4208
|
authorID: mail.authorID,
|
|
4136
4209
|
decrypted: true,
|
|
@@ -4140,18 +4213,16 @@ export class Client {
|
|
|
4140
4213
|
? uuid.stringify(mail.group)
|
|
4141
4214
|
: null,
|
|
4142
4215
|
mailID: mail.mailID,
|
|
4143
|
-
|
|
4216
|
+
...messageFromDecodedPlaintext(
|
|
4217
|
+
decodedPlaintext ?? {
|
|
4218
|
+
message: plaintext,
|
|
4219
|
+
},
|
|
4220
|
+
),
|
|
4144
4221
|
nonce: XUtils.encodeHex(
|
|
4145
4222
|
new Uint8Array(mail.nonce),
|
|
4146
4223
|
),
|
|
4147
4224
|
readerID: mail.readerID,
|
|
4148
4225
|
recipient: mail.recipient,
|
|
4149
|
-
...(hintFromEnvelope !== undefined
|
|
4150
|
-
? {
|
|
4151
|
-
retentionHintDays:
|
|
4152
|
-
hintFromEnvelope,
|
|
4153
|
-
}
|
|
4154
|
-
: {}),
|
|
4155
4226
|
sender: mail.sender,
|
|
4156
4227
|
timestamp: timestamp,
|
|
4157
4228
|
};
|
|
@@ -4391,34 +4462,14 @@ export class Client {
|
|
|
4391
4462
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
4392
4463
|
: null;
|
|
4393
4464
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
const stripped =
|
|
4398
|
-
stripVexRetentionEnvelope(rawIncoming);
|
|
4399
|
-
bodyIncoming = stripped.body;
|
|
4400
|
-
hintIncoming = stripped.retentionHintDays;
|
|
4401
|
-
}
|
|
4465
|
+
const decodedPlaintext = mail.forward
|
|
4466
|
+
? null
|
|
4467
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
4402
4468
|
const message: Message = fwdMsg2
|
|
4403
|
-
?
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
);
|
|
4408
|
-
const base: Message = {
|
|
4409
|
-
...fwdMsg2,
|
|
4410
|
-
forward: true,
|
|
4411
|
-
message: stripped.body,
|
|
4412
|
-
};
|
|
4413
|
-
return stripped.retentionHintDays !==
|
|
4414
|
-
undefined
|
|
4415
|
-
? {
|
|
4416
|
-
...base,
|
|
4417
|
-
retentionHintDays:
|
|
4418
|
-
stripped.retentionHintDays,
|
|
4419
|
-
}
|
|
4420
|
-
: base;
|
|
4421
|
-
})()
|
|
4469
|
+
? {
|
|
4470
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
4471
|
+
forward: true,
|
|
4472
|
+
}
|
|
4422
4473
|
: {
|
|
4423
4474
|
authorID: mail.authorID,
|
|
4424
4475
|
decrypted: true,
|
|
@@ -4428,15 +4479,16 @@ export class Client {
|
|
|
4428
4479
|
? uuid.stringify(mail.group)
|
|
4429
4480
|
: null,
|
|
4430
4481
|
mailID: mail.mailID,
|
|
4431
|
-
|
|
4482
|
+
...messageFromDecodedPlaintext(
|
|
4483
|
+
decodedPlaintext ?? {
|
|
4484
|
+
message: rawIncoming,
|
|
4485
|
+
},
|
|
4486
|
+
),
|
|
4432
4487
|
nonce: XUtils.encodeHex(
|
|
4433
4488
|
new Uint8Array(mail.nonce),
|
|
4434
4489
|
),
|
|
4435
4490
|
readerID: mail.readerID,
|
|
4436
4491
|
recipient: mail.recipient,
|
|
4437
|
-
...(hintIncoming !== undefined
|
|
4438
|
-
? { retentionHintDays: hintIncoming }
|
|
4439
|
-
: {}),
|
|
4440
4492
|
sender: mail.sender,
|
|
4441
4493
|
timestamp: timestamp,
|
|
4442
4494
|
};
|
|
@@ -4848,7 +4900,7 @@ export class Client {
|
|
|
4848
4900
|
private async sendGroupMessage(
|
|
4849
4901
|
channelID: string,
|
|
4850
4902
|
message: string,
|
|
4851
|
-
opts?:
|
|
4903
|
+
opts?: MessageSendOptions,
|
|
4852
4904
|
): Promise<void> {
|
|
4853
4905
|
const userList = await this.getUserList(channelID);
|
|
4854
4906
|
for (const user of userList) {
|
|
@@ -4856,10 +4908,7 @@ export class Client {
|
|
|
4856
4908
|
}
|
|
4857
4909
|
|
|
4858
4910
|
const mailID = uuid.v4();
|
|
4859
|
-
const payload =
|
|
4860
|
-
message,
|
|
4861
|
-
opts?.retentionHintDays,
|
|
4862
|
-
);
|
|
4911
|
+
const payload = encodeMessagePlaintext(message, opts);
|
|
4863
4912
|
const msgBytes = XUtils.decodeUTF8(payload);
|
|
4864
4913
|
const myUserID = this.getUser().userID;
|
|
4865
4914
|
const peerUserIDs = [...new Set(userList.map((u) => u.userID))].filter(
|
|
@@ -4890,6 +4939,7 @@ export class Client {
|
|
|
4890
4939
|
if (targetDevices.size === 0) {
|
|
4891
4940
|
const dev = this.getDevice();
|
|
4892
4941
|
const nonce = xMakeNonce();
|
|
4942
|
+
const decodedPlaintext = decodeMessagePlaintext(payload);
|
|
4893
4943
|
this.emitter.emit("message", {
|
|
4894
4944
|
authorID: myUserID,
|
|
4895
4945
|
decrypted: true,
|
|
@@ -4897,7 +4947,7 @@ export class Client {
|
|
|
4897
4947
|
forward: false,
|
|
4898
4948
|
group: channelID,
|
|
4899
4949
|
mailID,
|
|
4900
|
-
|
|
4950
|
+
...messageFromDecodedPlaintext(decodedPlaintext),
|
|
4901
4951
|
nonce: XUtils.encodeHex(nonce),
|
|
4902
4952
|
readerID: myUserID,
|
|
4903
4953
|
recipient: dev.deviceID,
|
|
@@ -5043,25 +5093,8 @@ export class Client {
|
|
|
5043
5093
|
const fwdOut = forward
|
|
5044
5094
|
? messageSchema.parse(msgpack.decode(msg))
|
|
5045
5095
|
: null;
|
|
5046
|
-
const rawUtf8 = XUtils.encodeUTF8(msg);
|
|
5047
|
-
const strippedOut = stripVexRetentionEnvelope(rawUtf8);
|
|
5048
5096
|
const outMsg: Message = fwdOut
|
|
5049
|
-
? (
|
|
5050
|
-
const stripped = stripVexRetentionEnvelope(
|
|
5051
|
-
fwdOut.message,
|
|
5052
|
-
);
|
|
5053
|
-
const base: Message = {
|
|
5054
|
-
...fwdOut,
|
|
5055
|
-
forward: true,
|
|
5056
|
-
message: stripped.body,
|
|
5057
|
-
};
|
|
5058
|
-
return stripped.retentionHintDays !== undefined
|
|
5059
|
-
? {
|
|
5060
|
-
...base,
|
|
5061
|
-
retentionHintDays: stripped.retentionHintDays,
|
|
5062
|
-
}
|
|
5063
|
-
: base;
|
|
5064
|
-
})()
|
|
5097
|
+
? { ...normalizeForwardedMessage(fwdOut), forward: true }
|
|
5065
5098
|
: {
|
|
5066
5099
|
authorID: mail.authorID,
|
|
5067
5100
|
decrypted: true,
|
|
@@ -5069,16 +5102,12 @@ export class Client {
|
|
|
5069
5102
|
forward: mail.forward,
|
|
5070
5103
|
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
5071
5104
|
mailID: mail.mailID,
|
|
5072
|
-
|
|
5105
|
+
...messageFromDecodedPlaintext(
|
|
5106
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(msg)),
|
|
5107
|
+
),
|
|
5073
5108
|
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
5074
5109
|
readerID: mail.readerID,
|
|
5075
5110
|
recipient: mail.recipient,
|
|
5076
|
-
...(strippedOut.retentionHintDays !== undefined
|
|
5077
|
-
? {
|
|
5078
|
-
retentionHintDays:
|
|
5079
|
-
strippedOut.retentionHintDays,
|
|
5080
|
-
}
|
|
5081
|
-
: {}),
|
|
5082
5111
|
sender: mail.sender,
|
|
5083
5112
|
timestamp: new Date().toISOString(),
|
|
5084
5113
|
};
|
|
@@ -5172,12 +5201,9 @@ export class Client {
|
|
|
5172
5201
|
private async sendMessage(
|
|
5173
5202
|
userID: string,
|
|
5174
5203
|
message: string,
|
|
5175
|
-
opts?:
|
|
5204
|
+
opts?: MessageSendOptions,
|
|
5176
5205
|
): Promise<void> {
|
|
5177
|
-
const payload =
|
|
5178
|
-
message,
|
|
5179
|
-
opts?.retentionHintDays,
|
|
5180
|
-
);
|
|
5206
|
+
const payload = encodeMessagePlaintext(message, opts);
|
|
5181
5207
|
try {
|
|
5182
5208
|
const [userEntry, err] = await this.fetchUser(userID);
|
|
5183
5209
|
if (err) {
|
|
@@ -91,6 +91,32 @@ export function platformSuite(
|
|
|
91
91
|
expect(msg.message).toBe("platform-test");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
test("send and receive DM with encrypted extra", async () => {
|
|
95
|
+
const me = client.me.user();
|
|
96
|
+
const extra = JSON.stringify({
|
|
97
|
+
reactionEvent: {
|
|
98
|
+
action: "toggle",
|
|
99
|
+
emoji: { kind: "unicode", value: "👍" },
|
|
100
|
+
targetMailID: "mail-target",
|
|
101
|
+
},
|
|
102
|
+
version: 1,
|
|
103
|
+
});
|
|
104
|
+
const msgPromise = waitForMessage(
|
|
105
|
+
client,
|
|
106
|
+
(m) =>
|
|
107
|
+
m.direction === "incoming" &&
|
|
108
|
+
m.decrypted &&
|
|
109
|
+
m.extra === extra,
|
|
110
|
+
`[${platformName}] self-DM extra`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await client.messages.send(me.userID, "", { extra });
|
|
114
|
+
const msg = await msgPromise;
|
|
115
|
+
|
|
116
|
+
expect(msg.message).toBe("");
|
|
117
|
+
expect(msg.extra).toBe(extra);
|
|
118
|
+
});
|
|
119
|
+
|
|
94
120
|
test("message history retrieve + delete", async () => {
|
|
95
121
|
const me = client.me.user();
|
|
96
122
|
const body = "history-test";
|
|
@@ -265,6 +291,14 @@ export function platformSuite(
|
|
|
265
291
|
);
|
|
266
292
|
|
|
267
293
|
// user1 sends group message, user2 receives it
|
|
294
|
+
const extra = JSON.stringify({
|
|
295
|
+
reactionEvent: {
|
|
296
|
+
action: "toggle",
|
|
297
|
+
emoji: { kind: "unicode", value: "🎉" },
|
|
298
|
+
targetMailID: "group-mail-target",
|
|
299
|
+
},
|
|
300
|
+
version: 1,
|
|
301
|
+
});
|
|
268
302
|
const msgPromise = waitForMessage(
|
|
269
303
|
client2,
|
|
270
304
|
(m) =>
|
|
@@ -275,10 +309,13 @@ export function platformSuite(
|
|
|
275
309
|
15_000,
|
|
276
310
|
);
|
|
277
311
|
await withTransientRetry(() =>
|
|
278
|
-
client.messages.group(channel.channelID, "hello channel"
|
|
312
|
+
client.messages.group(channel.channelID, "hello channel", {
|
|
313
|
+
extra,
|
|
314
|
+
}),
|
|
279
315
|
);
|
|
280
316
|
const msg = await msgPromise;
|
|
281
317
|
expect(msg.message).toBe("hello channel");
|
|
318
|
+
expect(msg.extra).toBe(extra);
|
|
282
319
|
|
|
283
320
|
// Cleanup
|
|
284
321
|
await client.servers.delete(server.serverID);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createMessageEmbedExtra,
|
|
10
|
+
type MessageEmbed,
|
|
11
|
+
parseMessageExtra,
|
|
12
|
+
serializeMessageExtra,
|
|
13
|
+
} from "../messageExtra.js";
|
|
14
|
+
|
|
15
|
+
const imageAttachment = {
|
|
16
|
+
contentType: "image/svg+xml",
|
|
17
|
+
fileID: "file-1",
|
|
18
|
+
fileName: "summary.svg",
|
|
19
|
+
fileSize: 2048,
|
|
20
|
+
key: "secret",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe("message extra", () => {
|
|
24
|
+
it("serializes and parses an encrypted media embed", () => {
|
|
25
|
+
const embed: MessageEmbed = {
|
|
26
|
+
blocks: [
|
|
27
|
+
{
|
|
28
|
+
attachment: imageAttachment,
|
|
29
|
+
mediaType: "svg",
|
|
30
|
+
title: "Workflow summary",
|
|
31
|
+
type: "media",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
source: "message",
|
|
35
|
+
type: "markdown",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
display: "decorate",
|
|
39
|
+
kind: "git.workflow",
|
|
40
|
+
title: "CI finished",
|
|
41
|
+
version: 1,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const extra = serializeMessageExtra({ embed, version: 1 });
|
|
45
|
+
expect(parseMessageExtra(extra).embed).toEqual(embed);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("merges embed metadata with existing reactions", () => {
|
|
49
|
+
const current = JSON.stringify({
|
|
50
|
+
reactions: [
|
|
51
|
+
{
|
|
52
|
+
emoji: { kind: "unicode", value: "👍" },
|
|
53
|
+
userIDs: ["alice"],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
version: 1,
|
|
57
|
+
});
|
|
58
|
+
const extra = createMessageEmbedExtra(
|
|
59
|
+
{
|
|
60
|
+
display: "replace",
|
|
61
|
+
kind: "voice.transcript",
|
|
62
|
+
title: "Voice memo transcript",
|
|
63
|
+
version: 1,
|
|
64
|
+
},
|
|
65
|
+
current,
|
|
66
|
+
);
|
|
67
|
+
const parsed = parseMessageExtra(extra);
|
|
68
|
+
|
|
69
|
+
expect(parsed.embed?.kind).toBe("voice.transcript");
|
|
70
|
+
expect(parsed.reactions).toEqual([
|
|
71
|
+
{
|
|
72
|
+
emoji: { kind: "unicode", value: "👍" },
|
|
73
|
+
userIDs: ["alice"],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("drops malformed known fields while preserving unknown metadata", () => {
|
|
79
|
+
const parsed = parseMessageExtra(
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
embed: { display: "replace", kind: 123 },
|
|
82
|
+
reactionEvent: { action: "toggle", targetMailID: "m1" },
|
|
83
|
+
vendor: { ok: true },
|
|
84
|
+
version: 999,
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(parsed).toEqual({
|
|
89
|
+
vendor: { ok: true },
|
|
90
|
+
version: 1,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns null for an empty serialized extra", () => {
|
|
95
|
+
expect(serializeMessageExtra({ version: 1 })).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type {
|
|
|
22
22
|
Me,
|
|
23
23
|
Message,
|
|
24
24
|
Messages,
|
|
25
|
+
MessageSendOptions,
|
|
25
26
|
Moderation,
|
|
26
27
|
NotificationSubscription,
|
|
27
28
|
NotificationSubscriptionInput,
|
|
@@ -47,6 +48,34 @@ export type {
|
|
|
47
48
|
HttpRequestRecord,
|
|
48
49
|
HttpResponse,
|
|
49
50
|
} from "./http.js";
|
|
51
|
+
export {
|
|
52
|
+
createMessageEmbedExtra,
|
|
53
|
+
MESSAGE_EXTRA_VERSION,
|
|
54
|
+
parseMessageExtra,
|
|
55
|
+
serializeMessageExtra,
|
|
56
|
+
} from "./messageExtra.js";
|
|
57
|
+
export type {
|
|
58
|
+
EncryptedFileAttachmentReference,
|
|
59
|
+
MessageEmbed,
|
|
60
|
+
MessageEmbedAction,
|
|
61
|
+
MessageEmbedBlock,
|
|
62
|
+
MessageEmbedCodeBlock,
|
|
63
|
+
MessageEmbedDisplay,
|
|
64
|
+
MessageEmbedDividerBlock,
|
|
65
|
+
MessageEmbedField,
|
|
66
|
+
MessageEmbedFileBlock,
|
|
67
|
+
MessageEmbedGalleryBlock,
|
|
68
|
+
MessageEmbedMarkdownBlock,
|
|
69
|
+
MessageEmbedMediaBlock,
|
|
70
|
+
MessageEmbedMediaItem,
|
|
71
|
+
MessageEmbedMediaType,
|
|
72
|
+
MessageEmbedSource,
|
|
73
|
+
MessageEmbedTone,
|
|
74
|
+
MessageEmoji,
|
|
75
|
+
MessageExtra,
|
|
76
|
+
MessageReaction,
|
|
77
|
+
MessageReactionEvent,
|
|
78
|
+
} from "./messageExtra.js";
|
|
50
79
|
export {
|
|
51
80
|
clampLocalMessageRetentionDays,
|
|
52
81
|
effectiveMessageRetentionHintDays,
|