@spectrum-ts/imessage 12.3.0 → 12.4.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 +110 -22
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -671,7 +671,7 @@ const getRemoteAttachment = async (client, guid) => {
|
|
|
671
671
|
};
|
|
672
672
|
//#endregion
|
|
673
673
|
//#region src/remote/inbound.ts
|
|
674
|
-
const log$
|
|
674
|
+
const log$4 = createLogger("spectrum.imessage.inbound");
|
|
675
675
|
const messageAttachments = (message) => message.content.attachments;
|
|
676
676
|
const resolveChatGuid = (message, hint) => {
|
|
677
677
|
if (hint) return hint;
|
|
@@ -735,7 +735,7 @@ const toVCardContent = async (client, info) => {
|
|
|
735
735
|
try {
|
|
736
736
|
return asContact(fromVCard((await downloadPrimaryAttachment(client, info.guid)).toString("utf8")));
|
|
737
737
|
} catch (err) {
|
|
738
|
-
log$
|
|
738
|
+
log$4.warn("failed to parse vCard attachment; falling back to attachment content", {
|
|
739
739
|
"spectrum.imessage.attachment.guid": info.guid,
|
|
740
740
|
...errorAttrs(err)
|
|
741
741
|
}, err);
|
|
@@ -823,7 +823,7 @@ const resolveReplyTarget = async (client, base, targetGuid, currentGuid, options
|
|
|
823
823
|
if (options.cache) cacheMessage(options.cache, rebuilt);
|
|
824
824
|
return rebuilt;
|
|
825
825
|
} catch (err) {
|
|
826
|
-
if (!(err instanceof NotFoundError)) log$
|
|
826
|
+
if (!(err instanceof NotFoundError)) log$4.warn("failed to resolve iMessage reply target; falling back to stub target", {
|
|
827
827
|
"spectrum.imessage.message.guid": currentGuid,
|
|
828
828
|
"spectrum.imessage.reply.target_guid": targetGuid,
|
|
829
829
|
...errorAttrs(err)
|
|
@@ -1503,7 +1503,7 @@ const randomPhone = (clients) => {
|
|
|
1503
1503
|
};
|
|
1504
1504
|
//#endregion
|
|
1505
1505
|
//#region src/remote/contact-share.ts
|
|
1506
|
-
const log$
|
|
1506
|
+
const log$3 = createLogger("spectrum.imessage.contact");
|
|
1507
1507
|
const SHARE_TTL_MS = 1440 * 60 * 1e3;
|
|
1508
1508
|
const MAX_TRACKED_CHATS = 1e4;
|
|
1509
1509
|
const isPreconditionFailure = (error) => typeof error === "object" && error !== null && "code" in error && error.code === ErrorCode.preconditionFailed;
|
|
@@ -1530,6 +1530,14 @@ var ContactShareTracker = class {
|
|
|
1530
1530
|
this.client = client;
|
|
1531
1531
|
}
|
|
1532
1532
|
/**
|
|
1533
|
+
* Whether this line has already attempted to share with the chat during the
|
|
1534
|
+
* 24-hour dedupe window. Callers use this before consulting any remote gate,
|
|
1535
|
+
* so repeat inbound messages stay entirely local.
|
|
1536
|
+
*/
|
|
1537
|
+
hasRecentlyShared(chatGuid) {
|
|
1538
|
+
return this.cache.has(chatGuid);
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1533
1541
|
* Best-effort share. The cache is set eagerly so that a burst of inbound
|
|
1534
1542
|
* messages for the same chat coalesces to a single API call. A
|
|
1535
1543
|
* `preconditionFailed` response remains cached for the normal 24-hour TTL,
|
|
@@ -1543,10 +1551,10 @@ var ContactShareTracker = class {
|
|
|
1543
1551
|
this.cache.set(chatGuid, true);
|
|
1544
1552
|
const safeChatGuid = sanitizeErrorMessage(chatGuid);
|
|
1545
1553
|
this.client.chats.shareContactInfo(chatGuid).then(() => {
|
|
1546
|
-
log$
|
|
1554
|
+
log$3.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
|
|
1547
1555
|
}).catch((error) => {
|
|
1548
1556
|
if (!isPreconditionFailure(error)) this.cache.delete(chatGuid);
|
|
1549
|
-
log$
|
|
1557
|
+
log$3.warn("failed to share contact card", {
|
|
1550
1558
|
"spectrum.imessage.contact.chat": safeChatGuid,
|
|
1551
1559
|
...errorAttrs(error)
|
|
1552
1560
|
}, error);
|
|
@@ -1572,7 +1580,7 @@ const getContactShareTracker = (client) => {
|
|
|
1572
1580
|
};
|
|
1573
1581
|
//#endregion
|
|
1574
1582
|
//#region src/remote/group-events.ts
|
|
1575
|
-
const log$
|
|
1583
|
+
const log$2 = createLogger("spectrum.imessage.group");
|
|
1576
1584
|
/**
|
|
1577
1585
|
* Synthetic id for a `group.changed` event — shared between the stream item
|
|
1578
1586
|
* (the dedup key across live/catch-up) and the surfaced message. `sequence`
|
|
@@ -1602,7 +1610,7 @@ const fetchIconContent = async (client, event) => {
|
|
|
1602
1610
|
}
|
|
1603
1611
|
});
|
|
1604
1612
|
} catch (e) {
|
|
1605
|
-
log$
|
|
1613
|
+
log$2.error("failed to fetch changed group icon", {
|
|
1606
1614
|
"spectrum.imessage.group.chat": event.chatGuid,
|
|
1607
1615
|
...errorAttrs(e)
|
|
1608
1616
|
}, e);
|
|
@@ -1657,7 +1665,7 @@ const toGroupEventMessages = async (client, event, phone) => {
|
|
|
1657
1665
|
};
|
|
1658
1666
|
//#endregion
|
|
1659
1667
|
//#region src/remote/polls.ts
|
|
1660
|
-
const log = createLogger("spectrum.imessage.poll");
|
|
1668
|
+
const log$1 = createLogger("spectrum.imessage.poll");
|
|
1661
1669
|
const isVotedPollEvent = (event) => event.delta.type === "voted";
|
|
1662
1670
|
const isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
|
|
1663
1671
|
const toCachedPoll = (input) => {
|
|
@@ -1689,7 +1697,7 @@ const cachePollEvent = (cache, event) => {
|
|
|
1689
1697
|
cache.set(event.pollMessageGuid, cached);
|
|
1690
1698
|
return cached;
|
|
1691
1699
|
} catch (e) {
|
|
1692
|
-
log.error("failed to cache poll", {
|
|
1700
|
+
log$1.error("failed to cache poll", {
|
|
1693
1701
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1694
1702
|
...errorAttrs(e)
|
|
1695
1703
|
}, e);
|
|
@@ -1701,7 +1709,7 @@ const fetchPollInfo = async (client, cache, event) => {
|
|
|
1701
1709
|
cachePollInfo(cache, info);
|
|
1702
1710
|
return info;
|
|
1703
1711
|
} catch (e) {
|
|
1704
|
-
log.error("failed to fetch poll", {
|
|
1712
|
+
log$1.error("failed to fetch poll", {
|
|
1705
1713
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1706
1714
|
...errorAttrs(e)
|
|
1707
1715
|
}, e);
|
|
@@ -1714,7 +1722,7 @@ const resolvePoll = async (client, cache, event) => {
|
|
|
1714
1722
|
try {
|
|
1715
1723
|
return cachePollInfo(cache, await client.polls.get(event.pollMessageGuid));
|
|
1716
1724
|
} catch (e) {
|
|
1717
|
-
log.error("failed to resolve poll", {
|
|
1725
|
+
log$1.error("failed to resolve poll", {
|
|
1718
1726
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1719
1727
|
...errorAttrs(e)
|
|
1720
1728
|
}, e);
|
|
@@ -1931,14 +1939,23 @@ const clientStream = (client, pollCache, phone, includeGroupEvents, onInbound, r
|
|
|
1931
1939
|
if (includeGroupEvents) streams.push(groupStream(client, phone, recover));
|
|
1932
1940
|
return mergeStreams(streams);
|
|
1933
1941
|
};
|
|
1934
|
-
const
|
|
1942
|
+
const shareWhenProfileSynced = (tracker, gate, chatGuid) => {
|
|
1943
|
+
if (tracker.hasRecentlyShared(chatGuid)) return;
|
|
1944
|
+
gate.isEnabled().then((enabled) => {
|
|
1945
|
+
if (enabled) tracker.maybeShare(chatGuid);
|
|
1946
|
+
}).catch((error) => {
|
|
1947
|
+
streamLog.warn("profile sync gate failed; skipping automatic contact sharing", errorAttrs(error), error instanceof Error ? error : void 0);
|
|
1948
|
+
});
|
|
1949
|
+
};
|
|
1950
|
+
const contactShareHandler = (tracker, profileSyncGate) => profileSyncGate ? (chatGuid) => shareWhenProfileSynced(tracker, profileSyncGate, chatGuid) : (chatGuid) => tracker.maybeShare(chatGuid);
|
|
1951
|
+
const messages$1 = (clients, projectConfig, profileSyncGate) => {
|
|
1935
1952
|
const pollCache = getPollCache(clients);
|
|
1936
|
-
const
|
|
1953
|
+
const staticShareEnabled = projectConfig?.profile?.imessageSynced === true;
|
|
1937
1954
|
const recover = getCloudRecover(clients);
|
|
1938
1955
|
const includeGroupEvents = !isSharedMode(clients);
|
|
1939
1956
|
return mergeStreams(clients.map((entry) => {
|
|
1940
|
-
const tracker =
|
|
1941
|
-
return clientStream(entry.client, pollCache, entry.phone, includeGroupEvents, tracker ? (
|
|
1957
|
+
const tracker = staticShareEnabled || profileSyncGate ? getContactShareTracker(entry.client) : void 0;
|
|
1958
|
+
return clientStream(entry.client, pollCache, entry.phone, includeGroupEvents, tracker ? contactShareHandler(tracker, profileSyncGate) : void 0, recover);
|
|
1942
1959
|
}));
|
|
1943
1960
|
};
|
|
1944
1961
|
//#endregion
|
|
@@ -1999,7 +2016,7 @@ const stopTyping$1 = async (remote, spaceId) => {
|
|
|
1999
2016
|
};
|
|
2000
2017
|
//#endregion
|
|
2001
2018
|
//#region src/remote/api.ts
|
|
2002
|
-
const messages = (clients, projectConfig) => messages$1(clients, projectConfig);
|
|
2019
|
+
const messages = (clients, projectConfig, profileSyncGate) => messages$1(clients, projectConfig, profileSyncGate);
|
|
2003
2020
|
const setBackground = async (remote, spaceId, content) => setBackground$1(remote, spaceId, content);
|
|
2004
2021
|
const sendCustomizedMiniApp = async (remote, spaceId, content) => sendCustomizedMiniApp$1(remote, spaceId, content);
|
|
2005
2022
|
const updateCustomizedMiniApp = async (remote, spaceId, session, content) => updateCustomizedMiniApp$1(remote, spaceId, session, content);
|
|
@@ -2058,6 +2075,73 @@ const toSpectrumMiniApp = (url, layout, live) => asCustomizedMiniApp({
|
|
|
2058
2075
|
...live === void 0 ? {} : { live }
|
|
2059
2076
|
});
|
|
2060
2077
|
//#endregion
|
|
2078
|
+
//#region src/remote/profile-sync-gate.ts
|
|
2079
|
+
const PROFILE_SYNC_CACHE_TTL_MS = 6e4;
|
|
2080
|
+
const PROFILE_SYNC_RETRY_DELAY_MS = 3e4;
|
|
2081
|
+
const log = createLogger("spectrum.imessage.profile-sync");
|
|
2082
|
+
const createProfileSyncGate = (options) => {
|
|
2083
|
+
const cacheTtlMs = options.cacheTtlMs ?? PROFILE_SYNC_CACHE_TTL_MS;
|
|
2084
|
+
const retryDelayMs = options.retryDelayMs ?? PROFILE_SYNC_RETRY_DELAY_MS;
|
|
2085
|
+
let cachedEnabled = options.initialEnabled;
|
|
2086
|
+
let disposed = false;
|
|
2087
|
+
let refreshInFlight;
|
|
2088
|
+
let retryAfter = 0;
|
|
2089
|
+
let validUntil = Date.now() + cacheTtlMs;
|
|
2090
|
+
const refreshNow = async () => {
|
|
2091
|
+
try {
|
|
2092
|
+
const enabled = await options.refresh();
|
|
2093
|
+
if (disposed) return false;
|
|
2094
|
+
cachedEnabled = enabled;
|
|
2095
|
+
retryAfter = 0;
|
|
2096
|
+
validUntil = Date.now() + cacheTtlMs;
|
|
2097
|
+
return enabled;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
cachedEnabled = false;
|
|
2100
|
+
retryAfter = Date.now() + retryDelayMs;
|
|
2101
|
+
validUntil = 0;
|
|
2102
|
+
log.warn("failed to refresh profile sync state; automatic contact sharing remains disabled", {
|
|
2103
|
+
"spectrum.imessage.profile_sync.retry_in_ms": retryDelayMs,
|
|
2104
|
+
...errorAttrs(error)
|
|
2105
|
+
}, error);
|
|
2106
|
+
return false;
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
const coalescedRefresh = () => {
|
|
2110
|
+
if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
|
|
2111
|
+
refreshInFlight = void 0;
|
|
2112
|
+
});
|
|
2113
|
+
return refreshInFlight;
|
|
2114
|
+
};
|
|
2115
|
+
return {
|
|
2116
|
+
dispose() {
|
|
2117
|
+
disposed = true;
|
|
2118
|
+
cachedEnabled = false;
|
|
2119
|
+
validUntil = 0;
|
|
2120
|
+
},
|
|
2121
|
+
async isEnabled() {
|
|
2122
|
+
if (disposed) return false;
|
|
2123
|
+
const now = Date.now();
|
|
2124
|
+
if (now < validUntil) return cachedEnabled;
|
|
2125
|
+
if (now < retryAfter) return false;
|
|
2126
|
+
return await coalescedRefresh();
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
};
|
|
2130
|
+
const gates = /* @__PURE__ */ new WeakMap();
|
|
2131
|
+
const isProfileSynced = (projectConfig) => projectConfig.profile?.imessageSynced === true;
|
|
2132
|
+
const registerProfileSyncGate = (clients, projectId, projectSecret, projectConfig) => {
|
|
2133
|
+
gates.get(clients)?.dispose();
|
|
2134
|
+
gates.set(clients, createProfileSyncGate({
|
|
2135
|
+
initialEnabled: isProfileSynced(projectConfig),
|
|
2136
|
+
refresh: async () => isProfileSynced(await cloud.getProject(projectId, projectSecret))
|
|
2137
|
+
}));
|
|
2138
|
+
};
|
|
2139
|
+
const getProfileSyncGate = (clients) => gates.get(clients);
|
|
2140
|
+
const disposeProfileSyncGate = (clients) => {
|
|
2141
|
+
gates.get(clients)?.dispose();
|
|
2142
|
+
gates.delete(clients);
|
|
2143
|
+
};
|
|
2144
|
+
//#endregion
|
|
2061
2145
|
//#region src/index.ts
|
|
2062
2146
|
const isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
|
|
2063
2147
|
const cacheRemoteOutbound = (remote, space, record) => {
|
|
@@ -2197,8 +2281,9 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2197
2281
|
config: configSchema,
|
|
2198
2282
|
static: { effect: { message: messageEffects } },
|
|
2199
2283
|
lifecycle: {
|
|
2200
|
-
createClient: async ({ config, projectId, projectSecret }) => {
|
|
2201
|
-
|
|
2284
|
+
createClient: async ({ config, projectConfig, projectId, projectSecret }) => {
|
|
2285
|
+
let clients;
|
|
2286
|
+
if (config.clients) clients = (Array.isArray(config.clients) ? config.clients : [config.clients]).map((e) => ({
|
|
2202
2287
|
phone: e.phone,
|
|
2203
2288
|
client: createGrpcClient({
|
|
2204
2289
|
address: e.address,
|
|
@@ -2208,10 +2293,13 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2208
2293
|
token: e.token
|
|
2209
2294
|
})
|
|
2210
2295
|
}));
|
|
2211
|
-
if (
|
|
2212
|
-
|
|
2296
|
+
else if (projectId && projectSecret) clients = await createCloudClients(projectId, projectSecret);
|
|
2297
|
+
else throw new Error("Cloud iMessage requires projectId and projectSecret. Pass credentials to Spectrum() or provide explicit clients with imessage.config({ clients: [...] }). For local Messages access, install @spectrum-ts/imessage-local and use localIMessage.config().");
|
|
2298
|
+
if (projectId && projectSecret && projectConfig) registerProfileSyncGate(clients, projectId, projectSecret, projectConfig);
|
|
2299
|
+
return clients;
|
|
2213
2300
|
},
|
|
2214
2301
|
destroyClient: async ({ client }) => {
|
|
2302
|
+
disposeProfileSyncGate(client);
|
|
2215
2303
|
await disposeCloudAuth(client);
|
|
2216
2304
|
await Promise.all(client.map((entry) => entry.client.close()));
|
|
2217
2305
|
}
|
|
@@ -2263,7 +2351,7 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2263
2351
|
}
|
|
2264
2352
|
},
|
|
2265
2353
|
message: { schema: messageSchema },
|
|
2266
|
-
messages: ({ client, projectConfig }) => messages(client, projectConfig),
|
|
2354
|
+
messages: ({ client, projectConfig }) => messages(client, projectConfig, getProfileSyncGate(client)),
|
|
2267
2355
|
send: async ({ space, content, client }) => {
|
|
2268
2356
|
if (content.type === "reply") {
|
|
2269
2357
|
const remote = remoteForMessageTarget(client, space, content.target, "reply", "replies");
|