@spectrum-ts/imessage 12.4.0 → 12.5.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/index.js +202 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ErrorCode, NotFoundError, ValidationError, createGrpcClient } from "@photon-ai/advanced-imessage/grpc";
|
|
2
2
|
import { sanitizePhone, withSpan } from "@photon-ai/otel";
|
|
3
3
|
import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read, text, toVCard } from "@spectrum-ts/core";
|
|
4
|
-
import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, asVoice, avatarSchema, buildPhotoAction, createLogger, createTokenRenewal, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
|
|
4
|
+
import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, asVoice, avatarSchema, buildPhotoAction, createLogger, createTokenRenewal, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, readSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
|
|
5
5
|
import z from "zod";
|
|
6
6
|
import { Marked } from "marked";
|
|
7
7
|
import { LRUCache } from "lru-cache";
|
|
@@ -409,6 +409,12 @@ const IMESSAGE_PLATFORM = "imessage";
|
|
|
409
409
|
const PART_PREFIX = /^p:(\d+)\//;
|
|
410
410
|
const dmChatGuid = (address) => `any;-;${address}`;
|
|
411
411
|
const chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
|
|
412
|
+
const dmPeerFromChatGuid = (guid) => {
|
|
413
|
+
if (chatTypeFromGuid(guid) !== "dm") return;
|
|
414
|
+
const separator = guid.indexOf(";-;");
|
|
415
|
+
if (separator < 0) return;
|
|
416
|
+
return guid.slice(separator + 3) || void 0;
|
|
417
|
+
};
|
|
412
418
|
const toChatGuid = (value) => value;
|
|
413
419
|
const toMessageGuid = (value) => value;
|
|
414
420
|
const formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
|
|
@@ -671,7 +677,7 @@ const getRemoteAttachment = async (client, guid) => {
|
|
|
671
677
|
};
|
|
672
678
|
//#endregion
|
|
673
679
|
//#region src/remote/inbound.ts
|
|
674
|
-
const log$
|
|
680
|
+
const log$5 = createLogger("spectrum.imessage.inbound");
|
|
675
681
|
const messageAttachments = (message) => message.content.attachments;
|
|
676
682
|
const resolveChatGuid = (message, hint) => {
|
|
677
683
|
if (hint) return hint;
|
|
@@ -735,7 +741,7 @@ const toVCardContent = async (client, info) => {
|
|
|
735
741
|
try {
|
|
736
742
|
return asContact(fromVCard((await downloadPrimaryAttachment(client, info.guid)).toString("utf8")));
|
|
737
743
|
} catch (err) {
|
|
738
|
-
log$
|
|
744
|
+
log$5.warn("failed to parse vCard attachment; falling back to attachment content", {
|
|
739
745
|
"spectrum.imessage.attachment.guid": info.guid,
|
|
740
746
|
...errorAttrs(err)
|
|
741
747
|
}, err);
|
|
@@ -823,7 +829,7 @@ const resolveReplyTarget = async (client, base, targetGuid, currentGuid, options
|
|
|
823
829
|
if (options.cache) cacheMessage(options.cache, rebuilt);
|
|
824
830
|
return rebuilt;
|
|
825
831
|
} catch (err) {
|
|
826
|
-
if (!(err instanceof NotFoundError)) log$
|
|
832
|
+
if (!(err instanceof NotFoundError)) log$5.warn("failed to resolve iMessage reply target; falling back to stub target", {
|
|
827
833
|
"spectrum.imessage.message.guid": currentGuid,
|
|
828
834
|
"spectrum.imessage.reply.target_guid": targetGuid,
|
|
829
835
|
...errorAttrs(err)
|
|
@@ -860,6 +866,36 @@ const cacheMessage = (cache, message) => {
|
|
|
860
866
|
for (const item of group.items) if (isIMessageMessage(item)) cache.set(item.id, item);
|
|
861
867
|
}
|
|
862
868
|
};
|
|
869
|
+
/**
|
|
870
|
+
* Resolve a guid to the spectrum message it maps to, for events that reference
|
|
871
|
+
* their target only by guid (reactions, read receipts). Cache first — outbound
|
|
872
|
+
* sends are cached at send time by `cacheRemoteOutbound`, inbound messages when
|
|
873
|
+
* they are converted — then one `messages.get` + rebuild, which also warms the
|
|
874
|
+
* cache so sibling events for the same guid (a group's other participants) are
|
|
875
|
+
* free.
|
|
876
|
+
*
|
|
877
|
+
* Any failure drops the event: these are secondary signals and must never wedge
|
|
878
|
+
* the stream. Deliberately unlike `getMessage` below (the public
|
|
879
|
+
* `space.getMessage` action), which rethrows non-`NotFoundError` failures; and
|
|
880
|
+
* unlike it this never resolves `p:N/guid` child ids, because event payloads
|
|
881
|
+
* always carry the parent guid.
|
|
882
|
+
*/
|
|
883
|
+
const resolveTargetMessage = async (client, cache, chatGuid, targetGuid, phone) => {
|
|
884
|
+
const cached = cache.get(targetGuid);
|
|
885
|
+
if (cached) return cached;
|
|
886
|
+
try {
|
|
887
|
+
const rebuilt = await rebuildFromAppleMessage(client, await client.messages.get(toMessageGuid(targetGuid)), phone, chatGuid);
|
|
888
|
+
cacheMessage(cache, rebuilt);
|
|
889
|
+
return rebuilt;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
log$5.debug("event target could not be resolved; dropping the event", {
|
|
892
|
+
"spectrum.imessage.target_guid": targetGuid,
|
|
893
|
+
"spectrum.imessage.chat_guid": chatGuid,
|
|
894
|
+
...errorAttrs(error)
|
|
895
|
+
}, error instanceof Error ? error : void 0);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
863
899
|
const toInboundMessages = async (client, cache, event, phone) => {
|
|
864
900
|
const base = buildMessageBase(event.message, event.chatGuid, event.occurredAt, phone);
|
|
865
901
|
const messageGuidStr = event.message.guid;
|
|
@@ -955,13 +991,8 @@ const asProviderReaction = (emoji, target) => reactionSchema.parse({
|
|
|
955
991
|
type: "reaction"
|
|
956
992
|
});
|
|
957
993
|
const resolveReactionTarget = async (client, cache, chat, targetGuid, partIndex, phone) => {
|
|
958
|
-
|
|
959
|
-
if (!candidate)
|
|
960
|
-
candidate = await rebuildFromAppleMessage(client, await client.messages.get(toMessageGuid(targetGuid)), phone, chat);
|
|
961
|
-
cacheMessage(cache, candidate);
|
|
962
|
-
} catch {
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
994
|
+
const candidate = await resolveTargetMessage(client, cache, chat, targetGuid, phone);
|
|
995
|
+
if (!candidate) return;
|
|
965
996
|
if (candidate.content.type === "group") {
|
|
966
997
|
const items = candidate.content.items;
|
|
967
998
|
if (!Array.isArray(items)) return candidate;
|
|
@@ -1503,7 +1534,7 @@ const randomPhone = (clients) => {
|
|
|
1503
1534
|
};
|
|
1504
1535
|
//#endregion
|
|
1505
1536
|
//#region src/remote/contact-share.ts
|
|
1506
|
-
const log$
|
|
1537
|
+
const log$4 = createLogger("spectrum.imessage.contact");
|
|
1507
1538
|
const SHARE_TTL_MS = 1440 * 60 * 1e3;
|
|
1508
1539
|
const MAX_TRACKED_CHATS = 1e4;
|
|
1509
1540
|
const isPreconditionFailure = (error) => typeof error === "object" && error !== null && "code" in error && error.code === ErrorCode.preconditionFailed;
|
|
@@ -1551,10 +1582,10 @@ var ContactShareTracker = class {
|
|
|
1551
1582
|
this.cache.set(chatGuid, true);
|
|
1552
1583
|
const safeChatGuid = sanitizeErrorMessage(chatGuid);
|
|
1553
1584
|
this.client.chats.shareContactInfo(chatGuid).then(() => {
|
|
1554
|
-
log$
|
|
1585
|
+
log$4.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
|
|
1555
1586
|
}).catch((error) => {
|
|
1556
1587
|
if (!isPreconditionFailure(error)) this.cache.delete(chatGuid);
|
|
1557
|
-
log$
|
|
1588
|
+
log$4.warn("failed to share contact card", {
|
|
1558
1589
|
"spectrum.imessage.contact.chat": safeChatGuid,
|
|
1559
1590
|
...errorAttrs(error)
|
|
1560
1591
|
}, error);
|
|
@@ -1580,7 +1611,7 @@ const getContactShareTracker = (client) => {
|
|
|
1580
1611
|
};
|
|
1581
1612
|
//#endregion
|
|
1582
1613
|
//#region src/remote/group-events.ts
|
|
1583
|
-
const log$
|
|
1614
|
+
const log$3 = createLogger("spectrum.imessage.group");
|
|
1584
1615
|
/**
|
|
1585
1616
|
* Synthetic id for a `group.changed` event — shared between the stream item
|
|
1586
1617
|
* (the dedup key across live/catch-up) and the surfaced message. `sequence`
|
|
@@ -1610,7 +1641,7 @@ const fetchIconContent = async (client, event) => {
|
|
|
1610
1641
|
}
|
|
1611
1642
|
});
|
|
1612
1643
|
} catch (e) {
|
|
1613
|
-
log$
|
|
1644
|
+
log$3.error("failed to fetch changed group icon", {
|
|
1614
1645
|
"spectrum.imessage.group.chat": event.chatGuid,
|
|
1615
1646
|
...errorAttrs(e)
|
|
1616
1647
|
}, e);
|
|
@@ -1665,7 +1696,7 @@ const toGroupEventMessages = async (client, event, phone) => {
|
|
|
1665
1696
|
};
|
|
1666
1697
|
//#endregion
|
|
1667
1698
|
//#region src/remote/polls.ts
|
|
1668
|
-
const log$
|
|
1699
|
+
const log$2 = createLogger("spectrum.imessage.poll");
|
|
1669
1700
|
const isVotedPollEvent = (event) => event.delta.type === "voted";
|
|
1670
1701
|
const isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
|
|
1671
1702
|
const toCachedPoll = (input) => {
|
|
@@ -1697,7 +1728,7 @@ const cachePollEvent = (cache, event) => {
|
|
|
1697
1728
|
cache.set(event.pollMessageGuid, cached);
|
|
1698
1729
|
return cached;
|
|
1699
1730
|
} catch (e) {
|
|
1700
|
-
log$
|
|
1731
|
+
log$2.error("failed to cache poll", {
|
|
1701
1732
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1702
1733
|
...errorAttrs(e)
|
|
1703
1734
|
}, e);
|
|
@@ -1709,7 +1740,7 @@ const fetchPollInfo = async (client, cache, event) => {
|
|
|
1709
1740
|
cachePollInfo(cache, info);
|
|
1710
1741
|
return info;
|
|
1711
1742
|
} catch (e) {
|
|
1712
|
-
log$
|
|
1743
|
+
log$2.error("failed to fetch poll", {
|
|
1713
1744
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1714
1745
|
...errorAttrs(e)
|
|
1715
1746
|
}, e);
|
|
@@ -1722,7 +1753,7 @@ const resolvePoll = async (client, cache, event) => {
|
|
|
1722
1753
|
try {
|
|
1723
1754
|
return cachePollInfo(cache, await client.polls.get(event.pollMessageGuid));
|
|
1724
1755
|
} catch (e) {
|
|
1725
|
-
log$
|
|
1756
|
+
log$2.error("failed to resolve poll", {
|
|
1726
1757
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1727
1758
|
...errorAttrs(e)
|
|
1728
1759
|
}, e);
|
|
@@ -1782,10 +1813,140 @@ const toPollDeltaMessages = async (client, pollCache, event, phone) => {
|
|
|
1782
1813
|
return [];
|
|
1783
1814
|
};
|
|
1784
1815
|
//#endregion
|
|
1816
|
+
//#region src/remote/read-receipts.ts
|
|
1817
|
+
const log$1 = createLogger("spectrum.imessage.read");
|
|
1818
|
+
/**
|
|
1819
|
+
* Synthetic id for a `message.read` event — shared between the stream item
|
|
1820
|
+
* (the dedup key across live/catch-up) and the surfaced message. `sequence` is
|
|
1821
|
+
* monotonic per line, so N participants reading the same message produce N
|
|
1822
|
+
* distinct ids, and the same event replayed through catch-up produces the id
|
|
1823
|
+
* it produced live.
|
|
1824
|
+
*/
|
|
1825
|
+
const readReceiptMessageId = (event) => `${event.messageGuid}:read:${event.sequence}`;
|
|
1826
|
+
const asProviderRead = (target) => readSchema.parse({
|
|
1827
|
+
target,
|
|
1828
|
+
type: "read"
|
|
1829
|
+
});
|
|
1830
|
+
/**
|
|
1831
|
+
* Whether the reader could be identified at all, decided without resolving
|
|
1832
|
+
* the target. A DM always can (the peer is in the guid); a group only if the
|
|
1833
|
+
* platform named an actor. Lets an unattributable group receipt drop before
|
|
1834
|
+
* paying for an RPC.
|
|
1835
|
+
*/
|
|
1836
|
+
const isAttributable = (event) => dmPeerFromChatGuid(event.chatGuid) !== void 0 || event.actor?.address !== void 0;
|
|
1837
|
+
/**
|
|
1838
|
+
* Identify the reader.
|
|
1839
|
+
*
|
|
1840
|
+
* `event.actor` is **not** the reader on this arm. Verified against a live
|
|
1841
|
+
* line: in a DM where the peer read our message, `actor` came back as *our
|
|
1842
|
+
* own* line's address, not theirs.
|
|
1843
|
+
*
|
|
1844
|
+
* So the chat guid comes first. In a DM there are exactly two participants
|
|
1845
|
+
* and the target is already known to be ours, which makes the reader
|
|
1846
|
+
* definitionally the other one — no address comparison needed, and therefore
|
|
1847
|
+
* correct on pooled lines too, where `phone` is the `"shared"` sentinel and
|
|
1848
|
+
* comparing it against a real address is meaningless.
|
|
1849
|
+
*
|
|
1850
|
+
* A group guid carries no participant list, so there `actor` is the only
|
|
1851
|
+
* possible attribution. It is trusted only when it names neither this line
|
|
1852
|
+
* nor the target's own sender; the latter is what catches the shared-mode
|
|
1853
|
+
* case, since the sentinel never equals a real address. An unattributable
|
|
1854
|
+
* receipt is dropped rather than surfaced with a wrong reader — the reader's
|
|
1855
|
+
* identity is the entire payload.
|
|
1856
|
+
*/
|
|
1857
|
+
const toReader = (event, target, phone) => {
|
|
1858
|
+
const peer = dmPeerFromChatGuid(event.chatGuid);
|
|
1859
|
+
if (peer) return {
|
|
1860
|
+
id: peer,
|
|
1861
|
+
address: peer
|
|
1862
|
+
};
|
|
1863
|
+
const actor = event.actor;
|
|
1864
|
+
if (actor?.address && actor.address !== phone && actor.address !== target.sender?.address) return toSenderRef(actor);
|
|
1865
|
+
};
|
|
1866
|
+
/**
|
|
1867
|
+
* Convert a `message.read` event into an inbound `read` message — someone read
|
|
1868
|
+
* a message the agent sent. `sender` is the reader; `content.target` is ours.
|
|
1869
|
+
*
|
|
1870
|
+
* Two guards, cheapest first:
|
|
1871
|
+
*
|
|
1872
|
+
* 1. **The reader must be identifiable -> else drop.** Unlike a membership
|
|
1873
|
+
* change (where the state change itself is the payload and
|
|
1874
|
+
* `sender: undefined` still carries meaning), the reader's identity *is*
|
|
1875
|
+
* the payload of a receipt. An unattributed receipt in a group is
|
|
1876
|
+
* indistinguishable noise and would corrupt any "N of M have read" tally.
|
|
1877
|
+
* Same call as `toReactionMessages`. See `toReader`.
|
|
1878
|
+
*
|
|
1879
|
+
* 2. **The target must be one of ours.** `event.isFromMe` is not trustworthy
|
|
1880
|
+
* here: the proto carries no comment for it on this arm, and it most
|
|
1881
|
+
* plausibly describes the *underlying message* — which for a genuine
|
|
1882
|
+
* receipt is ours, i.e. `true` — so keying self-suppression off it would
|
|
1883
|
+
* suppress every receipt worth surfacing. The durable invariant is that a
|
|
1884
|
+
* peer's receipt always points at a message we sent, so the resolved target
|
|
1885
|
+
* must be `direction: "outbound"`. That one check also suppresses the echo
|
|
1886
|
+
* of our own `chats.markRead()`, which marks *their* inbound messages and
|
|
1887
|
+
* therefore resolves `direction: "inbound"`.
|
|
1888
|
+
*/
|
|
1889
|
+
const toReadReceiptMessages = async (client, cache, event, phone) => {
|
|
1890
|
+
if (!isAttributable(event)) {
|
|
1891
|
+
log$1.debug("read receipt dropped: reader could not be identified", {
|
|
1892
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
1893
|
+
"spectrum.imessage.read.sequence": event.sequence,
|
|
1894
|
+
"spectrum.imessage.read.chat_guid": event.chatGuid,
|
|
1895
|
+
"spectrum.imessage.read.has_actor": false
|
|
1896
|
+
});
|
|
1897
|
+
return [];
|
|
1898
|
+
}
|
|
1899
|
+
const target = await resolveTargetMessage(client, cache, event.chatGuid, event.messageGuid, phone);
|
|
1900
|
+
if (!target) {
|
|
1901
|
+
log$1.debug("read receipt dropped: target message could not be resolved", {
|
|
1902
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
1903
|
+
"spectrum.imessage.read.sequence": event.sequence
|
|
1904
|
+
});
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
if (target.direction !== "outbound") {
|
|
1908
|
+
log$1.debug("read receipt dropped: target is not one of ours", {
|
|
1909
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
1910
|
+
"spectrum.imessage.read.sequence": event.sequence,
|
|
1911
|
+
"spectrum.imessage.read.target_direction": target.direction ?? "unset"
|
|
1912
|
+
});
|
|
1913
|
+
return [];
|
|
1914
|
+
}
|
|
1915
|
+
const reader = toReader(event, target, phone);
|
|
1916
|
+
if (!reader) {
|
|
1917
|
+
log$1.debug("read receipt dropped: reader could not be identified", {
|
|
1918
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
1919
|
+
"spectrum.imessage.read.sequence": event.sequence,
|
|
1920
|
+
"spectrum.imessage.read.chat_guid": event.chatGuid,
|
|
1921
|
+
"spectrum.imessage.read.has_actor": true
|
|
1922
|
+
});
|
|
1923
|
+
return [];
|
|
1924
|
+
}
|
|
1925
|
+
const readAt = event.readAt;
|
|
1926
|
+
log$1.debug("read receipt surfaced", {
|
|
1927
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
1928
|
+
"spectrum.imessage.read.sequence": event.sequence,
|
|
1929
|
+
"spectrum.imessage.read.reader": sanitizePhone(reader.id),
|
|
1930
|
+
"spectrum.imessage.read.used_read_at": readAt !== void 0
|
|
1931
|
+
});
|
|
1932
|
+
return [{
|
|
1933
|
+
id: readReceiptMessageId(event),
|
|
1934
|
+
content: asProviderRead(target),
|
|
1935
|
+
sender: reader,
|
|
1936
|
+
space: {
|
|
1937
|
+
id: event.chatGuid,
|
|
1938
|
+
type: chatTypeFromGuid(event.chatGuid),
|
|
1939
|
+
phone
|
|
1940
|
+
},
|
|
1941
|
+
timestamp: readAt ?? event.occurredAt
|
|
1942
|
+
}];
|
|
1943
|
+
};
|
|
1944
|
+
//#endregion
|
|
1785
1945
|
//#region src/remote/stream.ts
|
|
1786
1946
|
const isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
|
|
1787
1947
|
const streamLabel = (kind, phone) => `imessage.${kind}:${phone === "shared" ? phone : sanitizePhone(phone)}`;
|
|
1788
|
-
const
|
|
1948
|
+
const isActorCurrentAccount = (actor, phone) => phone !== "shared" && actor?.address !== void 0 && actor.address === phone;
|
|
1949
|
+
const isEventFromCurrentAccount = (event, phone) => event.isFromMe || isActorCurrentAccount(event.actor, phone);
|
|
1789
1950
|
const streamLog = createLogger("spectrum.imessage.stream");
|
|
1790
1951
|
const isRetryableMappingError = (error) => typeof error === "object" && error !== null && error.retryable === true;
|
|
1791
1952
|
const skipUnmappable = async (label, cursor, map) => {
|
|
@@ -1834,6 +1995,26 @@ const toMessageItem = async (client, event, phone, cursor, onInbound) => {
|
|
|
1834
1995
|
values: await toReactionMessages(client, cache, event, phone)
|
|
1835
1996
|
};
|
|
1836
1997
|
}
|
|
1998
|
+
if (event.type === "message.read") {
|
|
1999
|
+
const id = readReceiptMessageId(event);
|
|
2000
|
+
streamLog.debug("received a read event", {
|
|
2001
|
+
"spectrum.imessage.read.message_guid": event.messageGuid,
|
|
2002
|
+
"spectrum.imessage.read.sequence": event.sequence,
|
|
2003
|
+
"spectrum.imessage.read.chat_guid": event.chatGuid,
|
|
2004
|
+
"spectrum.imessage.read.actor": event.actor?.address ? sanitizePhone(event.actor.address) : "none",
|
|
2005
|
+
"spectrum.imessage.read.is_from_me": event.isFromMe,
|
|
2006
|
+
"spectrum.imessage.read.line": phone
|
|
2007
|
+
});
|
|
2008
|
+
return {
|
|
2009
|
+
cursor,
|
|
2010
|
+
id,
|
|
2011
|
+
values: await toReadReceiptMessages(client, getMessageCache(client), event, phone)
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
streamLog.debug("message event consumed without mapping", {
|
|
2015
|
+
"spectrum.imessage.event_type": event.type,
|
|
2016
|
+
"spectrum.imessage.sequence": event.sequence
|
|
2017
|
+
});
|
|
1837
2018
|
return {
|
|
1838
2019
|
cursor,
|
|
1839
2020
|
id: `${event.type}:${"messageGuid" in event ? event.messageGuid : "unknown"}:${event.sequence}`,
|