@spectrum-ts/imessage 12.2.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 +119 -28
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NotFoundError, ValidationError, createGrpcClient } from "@photon-ai/advanced-imessage/grpc";
|
|
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
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";
|
|
@@ -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,9 +1503,10 @@ 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
|
+
const isPreconditionFailure = (error) => typeof error === "object" && error !== null && "code" in error && error.code === ErrorCode.preconditionFailed;
|
|
1509
1510
|
/**
|
|
1510
1511
|
* Tracks which chats this bot's line has already proactively pushed its contact
|
|
1511
1512
|
* card to, so `im.chats.shareContactInfo` is fired at most once per chat per
|
|
@@ -1529,21 +1530,31 @@ var ContactShareTracker = class {
|
|
|
1529
1530
|
this.client = client;
|
|
1530
1531
|
}
|
|
1531
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
|
+
/**
|
|
1532
1541
|
* Best-effort share. The cache is set eagerly so that a burst of inbound
|
|
1533
|
-
* messages for the same chat coalesces to a single API call.
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1542
|
+
* messages for the same chat coalesces to a single API call. A
|
|
1543
|
+
* `preconditionFailed` response remains cached for the normal 24-hour TTL,
|
|
1544
|
+
* avoiding repeated attempts when the account cannot currently share its
|
|
1545
|
+
* profile. Other failures evict the entry so the next inbound retries.
|
|
1546
|
+
* Never awaits and never throws: the receive stream must not crash on share
|
|
1547
|
+
* failures.
|
|
1537
1548
|
*/
|
|
1538
1549
|
maybeShare(chatGuid) {
|
|
1539
1550
|
if (this.cache.has(chatGuid)) return;
|
|
1540
1551
|
this.cache.set(chatGuid, true);
|
|
1541
1552
|
const safeChatGuid = sanitizeErrorMessage(chatGuid);
|
|
1542
1553
|
this.client.chats.shareContactInfo(chatGuid).then(() => {
|
|
1543
|
-
log$
|
|
1554
|
+
log$3.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
|
|
1544
1555
|
}).catch((error) => {
|
|
1545
|
-
this.cache.delete(chatGuid);
|
|
1546
|
-
log$
|
|
1556
|
+
if (!isPreconditionFailure(error)) this.cache.delete(chatGuid);
|
|
1557
|
+
log$3.warn("failed to share contact card", {
|
|
1547
1558
|
"spectrum.imessage.contact.chat": safeChatGuid,
|
|
1548
1559
|
...errorAttrs(error)
|
|
1549
1560
|
}, error);
|
|
@@ -1569,7 +1580,7 @@ const getContactShareTracker = (client) => {
|
|
|
1569
1580
|
};
|
|
1570
1581
|
//#endregion
|
|
1571
1582
|
//#region src/remote/group-events.ts
|
|
1572
|
-
const log$
|
|
1583
|
+
const log$2 = createLogger("spectrum.imessage.group");
|
|
1573
1584
|
/**
|
|
1574
1585
|
* Synthetic id for a `group.changed` event — shared between the stream item
|
|
1575
1586
|
* (the dedup key across live/catch-up) and the surfaced message. `sequence`
|
|
@@ -1599,7 +1610,7 @@ const fetchIconContent = async (client, event) => {
|
|
|
1599
1610
|
}
|
|
1600
1611
|
});
|
|
1601
1612
|
} catch (e) {
|
|
1602
|
-
log$
|
|
1613
|
+
log$2.error("failed to fetch changed group icon", {
|
|
1603
1614
|
"spectrum.imessage.group.chat": event.chatGuid,
|
|
1604
1615
|
...errorAttrs(e)
|
|
1605
1616
|
}, e);
|
|
@@ -1654,7 +1665,7 @@ const toGroupEventMessages = async (client, event, phone) => {
|
|
|
1654
1665
|
};
|
|
1655
1666
|
//#endregion
|
|
1656
1667
|
//#region src/remote/polls.ts
|
|
1657
|
-
const log = createLogger("spectrum.imessage.poll");
|
|
1668
|
+
const log$1 = createLogger("spectrum.imessage.poll");
|
|
1658
1669
|
const isVotedPollEvent = (event) => event.delta.type === "voted";
|
|
1659
1670
|
const isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
|
|
1660
1671
|
const toCachedPoll = (input) => {
|
|
@@ -1686,7 +1697,7 @@ const cachePollEvent = (cache, event) => {
|
|
|
1686
1697
|
cache.set(event.pollMessageGuid, cached);
|
|
1687
1698
|
return cached;
|
|
1688
1699
|
} catch (e) {
|
|
1689
|
-
log.error("failed to cache poll", {
|
|
1700
|
+
log$1.error("failed to cache poll", {
|
|
1690
1701
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1691
1702
|
...errorAttrs(e)
|
|
1692
1703
|
}, e);
|
|
@@ -1698,7 +1709,7 @@ const fetchPollInfo = async (client, cache, event) => {
|
|
|
1698
1709
|
cachePollInfo(cache, info);
|
|
1699
1710
|
return info;
|
|
1700
1711
|
} catch (e) {
|
|
1701
|
-
log.error("failed to fetch poll", {
|
|
1712
|
+
log$1.error("failed to fetch poll", {
|
|
1702
1713
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1703
1714
|
...errorAttrs(e)
|
|
1704
1715
|
}, e);
|
|
@@ -1711,7 +1722,7 @@ const resolvePoll = async (client, cache, event) => {
|
|
|
1711
1722
|
try {
|
|
1712
1723
|
return cachePollInfo(cache, await client.polls.get(event.pollMessageGuid));
|
|
1713
1724
|
} catch (e) {
|
|
1714
|
-
log.error("failed to resolve poll", {
|
|
1725
|
+
log$1.error("failed to resolve poll", {
|
|
1715
1726
|
"spectrum.imessage.poll.guid": event.pollMessageGuid,
|
|
1716
1727
|
...errorAttrs(e)
|
|
1717
1728
|
}, e);
|
|
@@ -1928,14 +1939,23 @@ const clientStream = (client, pollCache, phone, includeGroupEvents, onInbound, r
|
|
|
1928
1939
|
if (includeGroupEvents) streams.push(groupStream(client, phone, recover));
|
|
1929
1940
|
return mergeStreams(streams);
|
|
1930
1941
|
};
|
|
1931
|
-
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) => {
|
|
1932
1952
|
const pollCache = getPollCache(clients);
|
|
1933
|
-
const
|
|
1953
|
+
const staticShareEnabled = projectConfig?.profile?.imessageSynced === true;
|
|
1934
1954
|
const recover = getCloudRecover(clients);
|
|
1935
1955
|
const includeGroupEvents = !isSharedMode(clients);
|
|
1936
1956
|
return mergeStreams(clients.map((entry) => {
|
|
1937
|
-
const tracker =
|
|
1938
|
-
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);
|
|
1939
1959
|
}));
|
|
1940
1960
|
};
|
|
1941
1961
|
//#endregion
|
|
@@ -1996,7 +2016,7 @@ const stopTyping$1 = async (remote, spaceId) => {
|
|
|
1996
2016
|
};
|
|
1997
2017
|
//#endregion
|
|
1998
2018
|
//#region src/remote/api.ts
|
|
1999
|
-
const messages = (clients, projectConfig) => messages$1(clients, projectConfig);
|
|
2019
|
+
const messages = (clients, projectConfig, profileSyncGate) => messages$1(clients, projectConfig, profileSyncGate);
|
|
2000
2020
|
const setBackground = async (remote, spaceId, content) => setBackground$1(remote, spaceId, content);
|
|
2001
2021
|
const sendCustomizedMiniApp = async (remote, spaceId, content) => sendCustomizedMiniApp$1(remote, spaceId, content);
|
|
2002
2022
|
const updateCustomizedMiniApp = async (remote, spaceId, session, content) => updateCustomizedMiniApp$1(remote, spaceId, session, content);
|
|
@@ -2055,6 +2075,73 @@ const toSpectrumMiniApp = (url, layout, live) => asCustomizedMiniApp({
|
|
|
2055
2075
|
...live === void 0 ? {} : { live }
|
|
2056
2076
|
});
|
|
2057
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
|
|
2058
2145
|
//#region src/index.ts
|
|
2059
2146
|
const isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
|
|
2060
2147
|
const cacheRemoteOutbound = (remote, space, record) => {
|
|
@@ -2194,8 +2281,9 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2194
2281
|
config: configSchema,
|
|
2195
2282
|
static: { effect: { message: messageEffects } },
|
|
2196
2283
|
lifecycle: {
|
|
2197
|
-
createClient: async ({ config, projectId, projectSecret }) => {
|
|
2198
|
-
|
|
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) => ({
|
|
2199
2287
|
phone: e.phone,
|
|
2200
2288
|
client: createGrpcClient({
|
|
2201
2289
|
address: e.address,
|
|
@@ -2205,10 +2293,13 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2205
2293
|
token: e.token
|
|
2206
2294
|
})
|
|
2207
2295
|
}));
|
|
2208
|
-
if (
|
|
2209
|
-
|
|
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;
|
|
2210
2300
|
},
|
|
2211
2301
|
destroyClient: async ({ client }) => {
|
|
2302
|
+
disposeProfileSyncGate(client);
|
|
2212
2303
|
await disposeCloudAuth(client);
|
|
2213
2304
|
await Promise.all(client.map((entry) => entry.client.close()));
|
|
2214
2305
|
}
|
|
@@ -2260,7 +2351,7 @@ const imessage = definePlatform(IMESSAGE_PLATFORM, {
|
|
|
2260
2351
|
}
|
|
2261
2352
|
},
|
|
2262
2353
|
message: { schema: messageSchema },
|
|
2263
|
-
messages: ({ client, projectConfig }) => messages(client, projectConfig),
|
|
2354
|
+
messages: ({ client, projectConfig }) => messages(client, projectConfig, getProfileSyncGate(client)),
|
|
2264
2355
|
send: async ({ space, content, client }) => {
|
|
2265
2356
|
if (content.type === "reply") {
|
|
2266
2357
|
const remote = remoteForMessageTarget(client, space, content.target, "reply", "replies");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectrum-ts/imessage",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.4.0",
|
|
4
4
|
"description": "iMessage provider for spectrum-ts.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@grpc/grpc-js": "^1.14.4",
|
|
35
35
|
"@photon-ai/advanced-imessage": "^2.0.2",
|
|
36
|
-
"@photon-ai/otel": "^3.
|
|
36
|
+
"@photon-ai/otel": "^3.3.0",
|
|
37
37
|
"lru-cache": "^11.0.0",
|
|
38
38
|
"marked": "^18.0.5",
|
|
39
39
|
"nice-grpc": "^2.1.16",
|