mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 18:51:23 +00:00
* feat: Make BlueBubbles the primary iMessage integration - Remove old imsg skill (skills/imsg/SKILL.md) - Create new BlueBubbles skill (skills/bluebubbles/SKILL.md) with message tool examples - Add keep-alive script documentation for VM/headless setups to docs/channels/bluebubbles.md - AppleScript that pokes Messages.app every 5 minutes - LaunchAgent configuration for automatic execution - Prevents Messages.app from going idle in VM environments - Update all documentation to prioritize BlueBubbles over legacy imsg: - Mark imsg channel as legacy throughout docs - Update README.md channel lists - Update wizard, hubs, pairing, and index docs - Update FAQ to recommend BlueBubbles for iMessage - Update RPC docs to note imsg as legacy pattern - Update Chinese documentation (zh-CN) - Replace imsg examples with generic macOS skill examples where appropriate BlueBubbles is now the recommended first-class iMessage integration, with the legacy imsg integration marked for potential future removal. * refactor: Update import paths and improve code formatting - Adjusted import paths in session-status-tool.ts, whatsapp-heartbeat.ts, and heartbeat-runner.ts for consistency. - Reformatted code for better readability by aligning and grouping related imports and function parameters. - Enhanced error messages and conditional checks for clarity in heartbeat-runner.ts. * skills: restore imsg skill and align bluebubbles skill * docs: update FAQ for clarity and formatting - Adjusted the formatting of the FAQ section to ensure consistent bullet point alignment. - No content changes were made, only formatting improvements for better readability. * style: oxfmt touched files * fix: preserve BlueBubbles developer reference (#8415) (thanks @tyler6204)
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import type { OpenClawConfig } from "../../config/config.js";
|
|
import { loadSessionStore, resolveStorePath } from "../../config/sessions.js";
|
|
import { normalizeE164 } from "../../utils.js";
|
|
import { normalizeChatChannelId } from "../registry.js";
|
|
|
|
type HeartbeatRecipientsResult = { recipients: string[]; source: string };
|
|
type HeartbeatRecipientsOpts = { to?: string; all?: boolean };
|
|
|
|
function getSessionRecipients(cfg: OpenClawConfig) {
|
|
const sessionCfg = cfg.session;
|
|
const scope = sessionCfg?.scope ?? "per-sender";
|
|
if (scope === "global") {
|
|
return [];
|
|
}
|
|
const storePath = resolveStorePath(cfg.session?.store);
|
|
const store = loadSessionStore(storePath);
|
|
const isGroupKey = (key: string) =>
|
|
key.includes(":group:") || key.includes(":channel:") || key.includes("@g.us");
|
|
const isCronKey = (key: string) => key.startsWith("cron:");
|
|
|
|
const recipients = Object.entries(store)
|
|
.filter(([key]) => key !== "global" && key !== "unknown")
|
|
.filter(([key]) => !isGroupKey(key) && !isCronKey(key))
|
|
.map(([_, entry]) => ({
|
|
to:
|
|
normalizeChatChannelId(entry?.lastChannel) === "whatsapp" && entry?.lastTo
|
|
? normalizeE164(entry.lastTo)
|
|
: "",
|
|
updatedAt: entry?.updatedAt ?? 0,
|
|
}))
|
|
.filter(({ to }) => to.length > 1)
|
|
.toSorted((a, b) => b.updatedAt - a.updatedAt);
|
|
|
|
// Dedupe while preserving recency ordering.
|
|
const seen = new Set<string>();
|
|
return recipients.filter((r) => {
|
|
if (seen.has(r.to)) {
|
|
return false;
|
|
}
|
|
seen.add(r.to);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
export function resolveWhatsAppHeartbeatRecipients(
|
|
cfg: OpenClawConfig,
|
|
opts: HeartbeatRecipientsOpts = {},
|
|
): HeartbeatRecipientsResult {
|
|
if (opts.to) {
|
|
return { recipients: [normalizeE164(opts.to)], source: "flag" };
|
|
}
|
|
|
|
const sessionRecipients = getSessionRecipients(cfg);
|
|
const allowFrom =
|
|
Array.isArray(cfg.channels?.whatsapp?.allowFrom) && cfg.channels.whatsapp.allowFrom.length > 0
|
|
? cfg.channels.whatsapp.allowFrom.filter((v) => v !== "*").map(normalizeE164)
|
|
: [];
|
|
|
|
const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
|
|
|
|
if (opts.all) {
|
|
const all = unique([...sessionRecipients.map((s) => s.to), ...allowFrom]);
|
|
return { recipients: all, source: "all" };
|
|
}
|
|
|
|
if (sessionRecipients.length === 1) {
|
|
return { recipients: [sessionRecipients[0].to], source: "session-single" };
|
|
}
|
|
if (sessionRecipients.length > 1) {
|
|
return {
|
|
recipients: sessionRecipients.map((s) => s.to),
|
|
source: "session-ambiguous",
|
|
};
|
|
}
|
|
|
|
return { recipients: allowFrom, source: "allowFrom" };
|
|
}
|