






























@@ -0,0 +1,176 @@
1+import { getActivePluginChannelRegistry } from "../../plugins/runtime.js";
2+import {
3+normalizeLowercaseStringOrEmpty,
4+normalizeOptionalString,
5+} from "../../shared/string-coerce.js";
6+import { resolveTextCommand } from "../commands-registry.js";
7+import { resolveCommandSurfaceChannel } from "./channel-context.js";
8+import { persistSessionEntry } from "./commands-session-store.js";
9+import type { CommandHandler, HandleCommandsParams } from "./commands-types.js";
10+11+const DOCK_KEY_PREFIX = "dock:";
12+13+type LinkedDockTarget = {
14+peerId: string;
15+};
16+17+function resolveDockCommandTarget(params: HandleCommandsParams): string | null {
18+const resolved = resolveTextCommand(params.command.commandBodyNormalized, params.cfg);
19+if (!resolved?.command.key.startsWith(DOCK_KEY_PREFIX)) {
20+return null;
21+}
22+if (resolved.command.category !== "docks") {
23+return null;
24+}
25+const target = normalizeLowercaseStringOrEmpty(
26+resolved.command.key.slice(DOCK_KEY_PREFIX.length),
27+);
28+return target || null;
29+}
30+31+function resolveTargetChannelAccountId(
32+params: HandleCommandsParams,
33+targetChannel: string,
34+): string {
35+const plugin = getActivePluginChannelRegistry()?.channels.find(
36+(entry) => normalizeLowercaseStringOrEmpty(entry.plugin.id) === targetChannel,
37+)?.plugin;
38+return normalizeOptionalString(plugin?.config.defaultAccountId?.(params.cfg)) || "default";
39+}
40+41+function collectSourcePeerCandidates(params: HandleCommandsParams): string[] {
42+return [
43+params.ctx.NativeDirectUserId,
44+params.ctx.SenderId,
45+params.command.senderId,
46+params.ctx.SenderE164,
47+params.ctx.SenderUsername,
48+params.ctx.From,
49+params.command.from,
50+params.ctx.OriginatingTo,
51+params.ctx.To,
52+]
53+.map((value) => normalizeOptionalString(value))
54+.filter((value): value is string => Boolean(value));
55+}
56+57+function buildSourceIdentityCandidates(
58+params: HandleCommandsParams,
59+sourceChannel: string,
60+): Set<string> {
61+const candidates = new Set<string>();
62+for (const peerId of collectSourcePeerCandidates(params)) {
63+const raw = normalizeLowercaseStringOrEmpty(peerId);
64+if (raw) {
65+candidates.add(raw);
66+}
67+if (sourceChannel) {
68+const scoped = normalizeLowercaseStringOrEmpty(`${sourceChannel}:${peerId}`);
69+if (scoped) {
70+candidates.add(scoped);
71+}
72+}
73+}
74+return candidates;
75+}
76+77+function resolveLinkedDockTarget(params: {
78+identityLinks: Record<string, string[]> | undefined;
79+sourceCandidates: Set<string>;
80+targetChannel: string;
81+}): LinkedDockTarget | null {
82+if (!params.identityLinks || params.sourceCandidates.size === 0) {
83+return null;
84+}
85+const targetPrefix = `${params.targetChannel}:`;
86+for (const ids of Object.values(params.identityLinks)) {
87+if (!Array.isArray(ids)) {
88+continue;
89+}
90+const normalizedIds = ids.map((id) => normalizeLowercaseStringOrEmpty(id)).filter(Boolean);
91+if (!normalizedIds.some((id) => params.sourceCandidates.has(id))) {
92+continue;
93+}
94+for (const id of ids) {
95+const trimmed = normalizeOptionalString(id);
96+if (!trimmed) {
97+continue;
98+}
99+if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith(targetPrefix)) {
100+continue;
101+}
102+return {
103+peerId: trimmed.slice(targetPrefix.length).trim(),
104+};
105+}
106+}
107+return null;
108+}
109+110+export const handleDockCommand: CommandHandler = async (params, allowTextCommands) => {
111+if (!allowTextCommands) {
112+return null;
113+}
114+const targetChannel = resolveDockCommandTarget(params);
115+if (!targetChannel) {
116+return null;
117+}
118+if (!params.command.isAuthorizedSender) {
119+return { shouldContinue: false };
120+}
121+122+const sourceChannel = resolveCommandSurfaceChannel(params);
123+if (sourceChannel === targetChannel) {
124+return {
125+shouldContinue: false,
126+reply: { text: `Already docked to ${targetChannel}.` },
127+};
128+}
129+130+const sourceCandidates = buildSourceIdentityCandidates(params, sourceChannel);
131+if (sourceCandidates.size === 0) {
132+return {
133+shouldContinue: false,
134+reply: { text: `Cannot dock to ${targetChannel}: sender id is unavailable.` },
135+};
136+}
137+138+const target = resolveLinkedDockTarget({
139+identityLinks: params.cfg.session?.identityLinks,
140+ sourceCandidates,
141+ targetChannel,
142+});
143+if (!target?.peerId) {
144+return {
145+shouldContinue: false,
146+reply: {
147+text: `Cannot dock to ${targetChannel}: add this sender and a ${targetChannel}:... peer to session.identityLinks.`,
148+},
149+};
150+}
151+152+const sessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry;
153+if (!sessionEntry || !params.sessionStore || !params.sessionKey) {
154+return {
155+shouldContinue: false,
156+reply: { text: `Cannot dock to ${targetChannel}: no active session entry was found.` },
157+};
158+}
159+160+sessionEntry.lastChannel = targetChannel;
161+sessionEntry.lastTo = target.peerId;
162+sessionEntry.lastAccountId = resolveTargetChannelAccountId(params, targetChannel);
163+params.sessionEntry = sessionEntry;
164+const persisted = await persistSessionEntry(params);
165+if (!persisted) {
166+return {
167+shouldContinue: false,
168+reply: { text: `Cannot dock to ${targetChannel}: session route could not be saved.` },
169+};
170+}
171+172+return {
173+shouldContinue: false,
174+reply: { text: `Docked replies to ${targetChannel}.` },
175+};
176+};
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。