iMessage: lazy-load setup wizard surface
This commit is contained in:
parent
399b6f745a
commit
413d2ff3da
1
extensions/imessage/src/channel.runtime.ts
Normal file
1
extensions/imessage/src/channel.runtime.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { imessageSetupWizard } from "./setup-surface.js";
|
||||||
@ -28,10 +28,18 @@ import {
|
|||||||
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js";
|
||||||
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js";
|
||||||
import { getIMessageRuntime } from "./runtime.js";
|
import { getIMessageRuntime } from "./runtime.js";
|
||||||
import { imessageSetupAdapter, imessageSetupWizard } from "./setup-surface.js";
|
import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js";
|
||||||
|
|
||||||
const meta = getChatChannelMeta("imessage");
|
const meta = getChatChannelMeta("imessage");
|
||||||
|
|
||||||
|
async function loadIMessageChannelRuntime() {
|
||||||
|
return await import("./channel.runtime.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({
|
||||||
|
imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard,
|
||||||
|
}));
|
||||||
|
|
||||||
type IMessageSendFn = ReturnType<
|
type IMessageSendFn = ReturnType<
|
||||||
typeof getIMessageRuntime
|
typeof getIMessageRuntime
|
||||||
>["channel"]["imessage"]["sendMessageIMessage"];
|
>["channel"]["imessage"]["sendMessageIMessage"];
|
||||||
|
|||||||
236
extensions/imessage/src/setup-core.ts
Normal file
236
extensions/imessage/src/setup-core.ts
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js";
|
||||||
|
import {
|
||||||
|
parseOnboardingEntriesAllowingWildcard,
|
||||||
|
promptParsedAllowFromForScopedChannel,
|
||||||
|
setChannelDmPolicyWithAllowFrom,
|
||||||
|
setOnboardingChannelEnabled,
|
||||||
|
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||||
|
import {
|
||||||
|
applyAccountNameToChannelSection,
|
||||||
|
migrateBaseNameToDefaultAccount,
|
||||||
|
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||||
|
import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||||
|
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||||
|
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||||
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||||
|
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||||
|
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||||
|
import {
|
||||||
|
listIMessageAccountIds,
|
||||||
|
resolveDefaultIMessageAccountId,
|
||||||
|
resolveIMessageAccount,
|
||||||
|
} from "./accounts.js";
|
||||||
|
import { normalizeIMessageHandle } from "./targets.js";
|
||||||
|
|
||||||
|
const channel = "imessage" as const;
|
||||||
|
|
||||||
|
export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } {
|
||||||
|
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
|
||||||
|
const lower = entry.toLowerCase();
|
||||||
|
if (lower.startsWith("chat_id:")) {
|
||||||
|
const id = entry.slice("chat_id:".length).trim();
|
||||||
|
if (!/^\d+$/.test(id)) {
|
||||||
|
return { error: `Invalid chat_id: ${entry}` };
|
||||||
|
}
|
||||||
|
return { value: entry };
|
||||||
|
}
|
||||||
|
if (lower.startsWith("chat_guid:")) {
|
||||||
|
if (!entry.slice("chat_guid:".length).trim()) {
|
||||||
|
return { error: "Invalid chat_guid entry" };
|
||||||
|
}
|
||||||
|
return { value: entry };
|
||||||
|
}
|
||||||
|
if (lower.startsWith("chat_identifier:")) {
|
||||||
|
if (!entry.slice("chat_identifier:".length).trim()) {
|
||||||
|
return { error: "Invalid chat_identifier entry" };
|
||||||
|
}
|
||||||
|
return { value: entry };
|
||||||
|
}
|
||||||
|
if (!normalizeIMessageHandle(entry)) {
|
||||||
|
return { error: `Invalid handle: ${entry}` };
|
||||||
|
}
|
||||||
|
return { value: entry };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIMessageSetupPatch(input: {
|
||||||
|
cliPath?: string;
|
||||||
|
dbPath?: string;
|
||||||
|
service?: "imessage" | "sms" | "auto";
|
||||||
|
region?: string;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
||||||
|
...(input.dbPath ? { dbPath: input.dbPath } : {}),
|
||||||
|
...(input.service ? { service: input.service } : {}),
|
||||||
|
...(input.region ? { region: input.region } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptIMessageAllowFrom(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
accountId?: string;
|
||||||
|
}): Promise<OpenClawConfig> {
|
||||||
|
return promptParsedAllowFromForScopedChannel({
|
||||||
|
cfg: params.cfg,
|
||||||
|
channel,
|
||||||
|
accountId: params.accountId,
|
||||||
|
defaultAccountId: resolveDefaultIMessageAccountId(params.cfg),
|
||||||
|
prompter: params.prompter,
|
||||||
|
noteTitle: "iMessage allowlist",
|
||||||
|
noteLines: [
|
||||||
|
"Allowlist iMessage DMs by handle or chat target.",
|
||||||
|
"Examples:",
|
||||||
|
"- +15555550123",
|
||||||
|
"- user@example.com",
|
||||||
|
"- chat_id:123",
|
||||||
|
"- chat_guid:... or chat_identifier:...",
|
||||||
|
"Multiple entries: comma-separated.",
|
||||||
|
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
|
||||||
|
],
|
||||||
|
message: "iMessage allowFrom (handle or chat_id)",
|
||||||
|
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||||
|
parseEntries: parseIMessageAllowFromEntries,
|
||||||
|
getExistingAllowFrom: ({ cfg, accountId }) =>
|
||||||
|
resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imessageSetupAdapter: ChannelSetupAdapter = {
|
||||||
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||||
|
applyAccountName: ({ cfg, accountId, name }) =>
|
||||||
|
applyAccountNameToChannelSection({
|
||||||
|
cfg,
|
||||||
|
channelKey: channel,
|
||||||
|
accountId,
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||||
|
const namedConfig = applyAccountNameToChannelSection({
|
||||||
|
cfg,
|
||||||
|
channelKey: channel,
|
||||||
|
accountId,
|
||||||
|
name: input.name,
|
||||||
|
});
|
||||||
|
const next =
|
||||||
|
accountId !== DEFAULT_ACCOUNT_ID
|
||||||
|
? migrateBaseNameToDefaultAccount({
|
||||||
|
cfg: namedConfig,
|
||||||
|
channelKey: channel,
|
||||||
|
})
|
||||||
|
: namedConfig;
|
||||||
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
channels: {
|
||||||
|
...next.channels,
|
||||||
|
imessage: {
|
||||||
|
...next.channels?.imessage,
|
||||||
|
enabled: true,
|
||||||
|
...buildIMessageSetupPatch(input),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
channels: {
|
||||||
|
...next.channels,
|
||||||
|
imessage: {
|
||||||
|
...next.channels?.imessage,
|
||||||
|
enabled: true,
|
||||||
|
accounts: {
|
||||||
|
...next.channels?.imessage?.accounts,
|
||||||
|
[accountId]: {
|
||||||
|
...next.channels?.imessage?.accounts?.[accountId],
|
||||||
|
enabled: true,
|
||||||
|
...buildIMessageSetupPatch(input),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createIMessageSetupWizardProxy(
|
||||||
|
loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>,
|
||||||
|
) {
|
||||||
|
const imessageDmPolicy: ChannelOnboardingDmPolicy = {
|
||||||
|
label: "iMessage",
|
||||||
|
channel,
|
||||||
|
policyKey: "channels.imessage.dmPolicy",
|
||||||
|
allowFromKey: "channels.imessage.allowFrom",
|
||||||
|
getCurrent: (cfg: OpenClawConfig) => cfg.channels?.imessage?.dmPolicy ?? "pairing",
|
||||||
|
setPolicy: (cfg: OpenClawConfig, policy) =>
|
||||||
|
setChannelDmPolicyWithAllowFrom({
|
||||||
|
cfg,
|
||||||
|
channel,
|
||||||
|
dmPolicy: policy,
|
||||||
|
}),
|
||||||
|
promptAllowFrom: promptIMessageAllowFrom,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel,
|
||||||
|
status: {
|
||||||
|
configuredLabel: "configured",
|
||||||
|
unconfiguredLabel: "needs setup",
|
||||||
|
configuredHint: "imsg found",
|
||||||
|
unconfiguredHint: "imsg missing",
|
||||||
|
configuredScore: 1,
|
||||||
|
unconfiguredScore: 0,
|
||||||
|
resolveConfigured: ({ cfg }) =>
|
||||||
|
listIMessageAccountIds(cfg).some((accountId) => {
|
||||||
|
const account = resolveIMessageAccount({ cfg, accountId });
|
||||||
|
return Boolean(
|
||||||
|
account.config.cliPath ||
|
||||||
|
account.config.dbPath ||
|
||||||
|
account.config.allowFrom ||
|
||||||
|
account.config.service ||
|
||||||
|
account.config.region,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
resolveStatusLines: async (params) =>
|
||||||
|
(await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [],
|
||||||
|
resolveSelectionHint: async (params) =>
|
||||||
|
await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params),
|
||||||
|
resolveQuickstartScore: async (params) =>
|
||||||
|
await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params),
|
||||||
|
},
|
||||||
|
credentials: [],
|
||||||
|
textInputs: [
|
||||||
|
{
|
||||||
|
inputKey: "cliPath",
|
||||||
|
message: "imsg CLI path",
|
||||||
|
initialValue: ({ cfg, accountId }) =>
|
||||||
|
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||||
|
currentValue: ({ cfg, accountId }) =>
|
||||||
|
resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg",
|
||||||
|
shouldPrompt: async (params) => {
|
||||||
|
const input = (await loadWizard()).imessageSetupWizard.textInputs?.find(
|
||||||
|
(entry) => entry.inputKey === "cliPath",
|
||||||
|
);
|
||||||
|
return (await input?.shouldPrompt?.(params)) ?? false;
|
||||||
|
},
|
||||||
|
confirmCurrentValue: false,
|
||||||
|
applyCurrentValue: true,
|
||||||
|
helpTitle: "iMessage",
|
||||||
|
helpLines: ["imsg CLI path required to enable iMessage."],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
completionNote: {
|
||||||
|
title: "iMessage next steps",
|
||||||
|
lines: [
|
||||||
|
"This is still a work in progress.",
|
||||||
|
"Ensure OpenClaw has Full Disk Access to Messages DB.",
|
||||||
|
"Grant Automation permission for Messages when prompted.",
|
||||||
|
"List chats with: imsg chats --limit 20",
|
||||||
|
`Docs: ${formatDocsLink("/imessage", "imessage")}`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
dmPolicy: imessageDmPolicy,
|
||||||
|
disable: (cfg: OpenClawConfig) => setOnboardingChannelEnabled(cfg, channel, false),
|
||||||
|
} satisfies ChannelSetupWizard;
|
||||||
|
}
|
||||||
@ -5,15 +5,10 @@ import {
|
|||||||
setChannelDmPolicyWithAllowFrom,
|
setChannelDmPolicyWithAllowFrom,
|
||||||
setOnboardingChannelEnabled,
|
setOnboardingChannelEnabled,
|
||||||
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
} from "../../../src/channels/plugins/onboarding/helpers.js";
|
||||||
import {
|
|
||||||
applyAccountNameToChannelSection,
|
|
||||||
migrateBaseNameToDefaultAccount,
|
|
||||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
|
||||||
import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
|
||||||
import { detectBinary } from "../../../src/commands/onboard-helpers.js";
|
import { detectBinary } from "../../../src/commands/onboard-helpers.js";
|
||||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||||
import {
|
import {
|
||||||
@ -21,53 +16,10 @@ import {
|
|||||||
resolveDefaultIMessageAccountId,
|
resolveDefaultIMessageAccountId,
|
||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
} from "./accounts.js";
|
} from "./accounts.js";
|
||||||
import { normalizeIMessageHandle } from "./targets.js";
|
import { imessageSetupAdapter, parseIMessageAllowFromEntries } from "./setup-core.js";
|
||||||
|
|
||||||
const channel = "imessage" as const;
|
const channel = "imessage" as const;
|
||||||
|
|
||||||
export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } {
|
|
||||||
return parseOnboardingEntriesAllowingWildcard(raw, (entry) => {
|
|
||||||
const lower = entry.toLowerCase();
|
|
||||||
if (lower.startsWith("chat_id:")) {
|
|
||||||
const id = entry.slice("chat_id:".length).trim();
|
|
||||||
if (!/^\d+$/.test(id)) {
|
|
||||||
return { error: `Invalid chat_id: ${entry}` };
|
|
||||||
}
|
|
||||||
return { value: entry };
|
|
||||||
}
|
|
||||||
if (lower.startsWith("chat_guid:")) {
|
|
||||||
if (!entry.slice("chat_guid:".length).trim()) {
|
|
||||||
return { error: "Invalid chat_guid entry" };
|
|
||||||
}
|
|
||||||
return { value: entry };
|
|
||||||
}
|
|
||||||
if (lower.startsWith("chat_identifier:")) {
|
|
||||||
if (!entry.slice("chat_identifier:".length).trim()) {
|
|
||||||
return { error: "Invalid chat_identifier entry" };
|
|
||||||
}
|
|
||||||
return { value: entry };
|
|
||||||
}
|
|
||||||
if (!normalizeIMessageHandle(entry)) {
|
|
||||||
return { error: `Invalid handle: ${entry}` };
|
|
||||||
}
|
|
||||||
return { value: entry };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildIMessageSetupPatch(input: {
|
|
||||||
cliPath?: string;
|
|
||||||
dbPath?: string;
|
|
||||||
service?: "imessage" | "sms" | "auto";
|
|
||||||
region?: string;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
...(input.cliPath ? { cliPath: input.cliPath } : {}),
|
|
||||||
...(input.dbPath ? { dbPath: input.dbPath } : {}),
|
|
||||||
...(input.service ? { service: input.service } : {}),
|
|
||||||
...(input.region ? { region: input.region } : {}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promptIMessageAllowFrom(params: {
|
async function promptIMessageAllowFrom(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
prompter: WizardPrompter;
|
prompter: WizardPrompter;
|
||||||
@ -113,63 +65,6 @@ const imessageDmPolicy: ChannelOnboardingDmPolicy = {
|
|||||||
promptAllowFrom: promptIMessageAllowFrom,
|
promptAllowFrom: promptIMessageAllowFrom,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const imessageSetupAdapter: ChannelSetupAdapter = {
|
|
||||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
||||||
applyAccountName: ({ cfg, accountId, name }) =>
|
|
||||||
applyAccountNameToChannelSection({
|
|
||||||
cfg,
|
|
||||||
channelKey: channel,
|
|
||||||
accountId,
|
|
||||||
name,
|
|
||||||
}),
|
|
||||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
||||||
const namedConfig = applyAccountNameToChannelSection({
|
|
||||||
cfg,
|
|
||||||
channelKey: channel,
|
|
||||||
accountId,
|
|
||||||
name: input.name,
|
|
||||||
});
|
|
||||||
const next =
|
|
||||||
accountId !== DEFAULT_ACCOUNT_ID
|
|
||||||
? migrateBaseNameToDefaultAccount({
|
|
||||||
cfg: namedConfig,
|
|
||||||
channelKey: channel,
|
|
||||||
})
|
|
||||||
: namedConfig;
|
|
||||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
channels: {
|
|
||||||
...next.channels,
|
|
||||||
imessage: {
|
|
||||||
...next.channels?.imessage,
|
|
||||||
enabled: true,
|
|
||||||
...buildIMessageSetupPatch(input),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
channels: {
|
|
||||||
...next.channels,
|
|
||||||
imessage: {
|
|
||||||
...next.channels?.imessage,
|
|
||||||
enabled: true,
|
|
||||||
accounts: {
|
|
||||||
...next.channels?.imessage?.accounts,
|
|
||||||
[accountId]: {
|
|
||||||
...next.channels?.imessage?.accounts?.[accountId],
|
|
||||||
enabled: true,
|
|
||||||
...buildIMessageSetupPatch(input),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const imessageSetupWizard: ChannelSetupWizard = {
|
export const imessageSetupWizard: ChannelSetupWizard = {
|
||||||
channel,
|
channel,
|
||||||
status: {
|
status: {
|
||||||
@ -236,3 +131,5 @@ export const imessageSetupWizard: ChannelSetupWizard = {
|
|||||||
dmPolicy: imessageDmPolicy,
|
dmPolicy: imessageDmPolicy,
|
||||||
disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false),
|
disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { imessageSetupAdapter, parseIMessageAllowFromEntries };
|
||||||
|
|||||||
@ -24,10 +24,8 @@ export {
|
|||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
resolveIMessageGroupToolPolicy,
|
resolveIMessageGroupToolPolicy,
|
||||||
} from "../channels/plugins/group-mentions.js";
|
} from "../channels/plugins/group-mentions.js";
|
||||||
export {
|
export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js";
|
||||||
imessageSetupAdapter,
|
export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js";
|
||||||
imessageSetupWizard,
|
|
||||||
} from "../../extensions/imessage/src/setup-surface.js";
|
|
||||||
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
|
export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js";
|
||||||
|
|
||||||
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js";
|
||||||
|
|||||||
@ -706,10 +706,8 @@ export {
|
|||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
type ResolvedIMessageAccount,
|
type ResolvedIMessageAccount,
|
||||||
} from "../../extensions/imessage/src/accounts.js";
|
} from "../../extensions/imessage/src/accounts.js";
|
||||||
export {
|
export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js";
|
||||||
imessageSetupAdapter,
|
export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js";
|
||||||
imessageSetupWizard,
|
|
||||||
} from "../../extensions/imessage/src/setup-surface.js";
|
|
||||||
export {
|
export {
|
||||||
looksLikeIMessageTargetId,
|
looksLikeIMessageTargetId,
|
||||||
normalizeIMessageMessagingTarget,
|
normalizeIMessageMessagingTarget,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user