mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
refactor: unify DM pairing challenge flows
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
evictOldHistoryKeys,
|
||||
issuePairingChallenge,
|
||||
logAckFailure,
|
||||
logInboundDrop,
|
||||
logTypingFailure,
|
||||
@@ -595,25 +596,24 @@ export async function processMessage(
|
||||
}
|
||||
|
||||
if (accessDecision.decision === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: message.senderId,
|
||||
await issuePairingChallenge({
|
||||
channel: "bluebubbles",
|
||||
senderId: message.senderId,
|
||||
senderIdLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
||||
meta: { name: message.senderName },
|
||||
});
|
||||
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=${created}`);
|
||||
if (created) {
|
||||
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
|
||||
try {
|
||||
await sendMessageBlueBubbles(
|
||||
message.senderId,
|
||||
core.channel.pairing.buildPairingReply({
|
||||
channel: "bluebubbles",
|
||||
idLine: `Your BlueBubbles sender id: ${message.senderId}`,
|
||||
code,
|
||||
}),
|
||||
{ cfg: config, accountId: account.accountId },
|
||||
);
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
runtime.log?.(`[bluebubbles] pairing request sender=${message.senderId} created=true`);
|
||||
logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageBlueBubbles(message.senderId, text, {
|
||||
cfg: config,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
@@ -622,8 +622,8 @@ export async function processMessage(
|
||||
runtime.error?.(
|
||||
`[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createScopedPairingAccess,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
type HistoryEntry,
|
||||
issuePairingChallenge,
|
||||
normalizeAgentId,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveOpenProviderRuntimeGroupPolicy,
|
||||
@@ -1101,29 +1102,29 @@ export async function handleFeishuMessage(params: {
|
||||
|
||||
if (isDirect && dmPolicy !== "open" && !dmAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: ctx.senderOpenId,
|
||||
await issuePairingChallenge({
|
||||
channel: "feishu",
|
||||
senderId: ctx.senderOpenId,
|
||||
senderIdLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
||||
meta: { name: ctx.senderName },
|
||||
});
|
||||
if (created) {
|
||||
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
||||
try {
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
log(`feishu[${account.accountId}]: pairing request sender=${ctx.senderOpenId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageFeishu({
|
||||
cfg,
|
||||
to: `chat:${ctx.chatId}`,
|
||||
text: core.channel.pairing.buildPairingReply({
|
||||
channel: "feishu",
|
||||
idLine: `Your Feishu user id: ${ctx.senderOpenId}`,
|
||||
code,
|
||||
}),
|
||||
text,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
log(
|
||||
`feishu[${account.accountId}]: pairing reply failed for ${ctx.senderOpenId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
log(
|
||||
`feishu[${account.accountId}]: blocked unauthorized sender ${ctx.senderOpenId} (dmPolicy=${dmPolicy})`,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
createScopedPairingAccess,
|
||||
issuePairingChallenge,
|
||||
isDangerousNameMatchingEnabled,
|
||||
resolveAllowlistProviderRuntimeGroupPolicy,
|
||||
resolveDefaultGroupPolicy,
|
||||
@@ -311,27 +312,27 @@ export async function applyGoogleChatInboundAccessPolicy(params: {
|
||||
|
||||
if (access.decision !== "allow") {
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
await issuePairingChallenge({
|
||||
channel: "googlechat",
|
||||
senderId,
|
||||
senderIdLine: `Your Google Chat user id: ${senderId}`,
|
||||
meta: { name: senderName || undefined, email: senderEmail },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(`googlechat pairing request sender=${senderId}`);
|
||||
try {
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
logVerbose(`googlechat pairing request sender=${senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendGoogleChatMessage({
|
||||
account,
|
||||
space: spaceId,
|
||||
text: core.channel.pairing.buildPairingReply({
|
||||
channel: "googlechat",
|
||||
idLine: `Your Google Chat user id: ${senderId}`,
|
||||
code,
|
||||
}),
|
||||
text,
|
||||
});
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createScopedPairingAccess,
|
||||
dispatchInboundReplyWithBase,
|
||||
formatTextWithAttachmentLinks,
|
||||
issuePairingChallenge,
|
||||
logInboundDrop,
|
||||
isDangerousNameMatchingEnabled,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
@@ -208,28 +209,25 @@ export async function handleIrcInbound(params: {
|
||||
}).allowed;
|
||||
if (!dmAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderDisplay.toLowerCase(),
|
||||
await issuePairingChallenge({
|
||||
channel: CHANNEL_ID,
|
||||
senderId: senderDisplay.toLowerCase(),
|
||||
senderIdLine: `Your IRC id: ${senderDisplay}`,
|
||||
meta: { name: message.senderNick || undefined },
|
||||
});
|
||||
if (created) {
|
||||
try {
|
||||
const reply = core.channel.pairing.buildPairingReply({
|
||||
channel: CHANNEL_ID,
|
||||
idLine: `Your IRC id: ${senderDisplay}`,
|
||||
code,
|
||||
});
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
sendPairingReply: async (text) => {
|
||||
await deliverIrcReply({
|
||||
payload: { text: reply },
|
||||
payload: { text },
|
||||
target: message.senderNick,
|
||||
accountId: account.accountId,
|
||||
sendReply: params.sendReply,
|
||||
statusSink,
|
||||
});
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
runtime.error?.(`irc: pairing reply failed for ${senderDisplay}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
|
||||
return;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createScopedPairingAccess,
|
||||
dispatchInboundReplyWithBase,
|
||||
formatTextWithAttachmentLinks,
|
||||
issuePairingChallenge,
|
||||
logInboundDrop,
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithCommandGate,
|
||||
@@ -173,26 +174,20 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
} else {
|
||||
if (access.decision !== "allow") {
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
await issuePairingChallenge({
|
||||
channel: CHANNEL_ID,
|
||||
senderId,
|
||||
senderIdLine: `Your Nextcloud user id: ${senderId}`,
|
||||
meta: { name: senderName || undefined },
|
||||
});
|
||||
if (created) {
|
||||
try {
|
||||
await sendMessageNextcloudTalk(
|
||||
roomToken,
|
||||
core.channel.pairing.buildPairingReply({
|
||||
channel: CHANNEL_ID,
|
||||
idLine: `Your Nextcloud user id: ${senderId}`,
|
||||
code,
|
||||
}),
|
||||
{ accountId: account.accountId },
|
||||
);
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageNextcloudTalk(roomToken, text, { accountId: account.accountId });
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
runtime.error?.(`nextcloud-talk: pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
runtime.log?.(`nextcloud-talk: drop DM sender ${senderId} (reason=${access.reason})`);
|
||||
return;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import {
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
issuePairingChallenge,
|
||||
resolveDirectDmAuthorizationOutcome,
|
||||
resolveSenderCommandAuthorizationWithRuntime,
|
||||
resolveOutboundMediaUrls,
|
||||
@@ -414,31 +415,30 @@ async function processMessageWithPipeline(params: {
|
||||
}
|
||||
if (directDmOutcome === "unauthorized") {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
await issuePairingChallenge({
|
||||
channel: "zalo",
|
||||
senderId,
|
||||
senderIdLine: `Your Zalo user id: ${senderId}`,
|
||||
meta: { name: senderName ?? undefined },
|
||||
});
|
||||
|
||||
if (created) {
|
||||
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
||||
try {
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
logVerbose(core, runtime, `zalo pairing request sender=${senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessage(
|
||||
token,
|
||||
{
|
||||
chat_id: chatId,
|
||||
text: core.channel.pairing.buildPairingReply({
|
||||
channel: "zalo",
|
||||
idLine: `Your Zalo user id: ${senderId}`,
|
||||
code,
|
||||
}),
|
||||
text,
|
||||
},
|
||||
fetcher,
|
||||
);
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(core, runtime, `zalo pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logVerbose(
|
||||
core,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
createTypingCallbacks,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
issuePairingChallenge,
|
||||
resolveOutboundMediaUrls,
|
||||
mergeAllowlist,
|
||||
resolveMentionGatingWithBypass,
|
||||
@@ -262,32 +263,27 @@ async function processMessage(
|
||||
const allowed = senderAllowedForCommands;
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
await issuePairingChallenge({
|
||||
channel: "zalouser",
|
||||
senderId,
|
||||
senderIdLine: `Your Zalo user id: ${senderId}`,
|
||||
meta: { name: senderName || undefined },
|
||||
});
|
||||
|
||||
if (created) {
|
||||
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
|
||||
try {
|
||||
await sendMessageZalouser(
|
||||
chatId,
|
||||
core.channel.pairing.buildPairingReply({
|
||||
channel: "zalouser",
|
||||
idLine: `Your Zalo user id: ${senderId}`,
|
||||
code,
|
||||
}),
|
||||
{ profile: account.profile },
|
||||
);
|
||||
upsertPairingRequest: pairing.upsertPairingRequest,
|
||||
onCreated: () => {
|
||||
logVerbose(core, runtime, `zalouser pairing request sender=${senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageZalouser(chatId, text, { profile: account.profile });
|
||||
statusSink?.({ lastOutboundAt: Date.now() });
|
||||
} catch (err) {
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(
|
||||
core,
|
||||
runtime,
|
||||
`zalouser pairing reply failed for ${senderId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
logVerbose(
|
||||
core,
|
||||
|
||||
@@ -35,7 +35,7 @@ import { logVerbose } from "../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { logDebug, logError } from "../../logger.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import { createNonExitingRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
@@ -519,28 +519,37 @@ async function ensureDmComponentAuthorized(params: {
|
||||
}
|
||||
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
const pairingResult = await issuePairingChallenge({
|
||||
channel: "discord",
|
||||
id: user.id,
|
||||
accountId: ctx.accountId,
|
||||
senderId: user.id,
|
||||
senderIdLine: `Your Discord user id: ${user.id}`,
|
||||
meta: {
|
||||
tag: formatDiscordUserTag(user),
|
||||
name: user.username,
|
||||
},
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "discord",
|
||||
id,
|
||||
accountId: ctx.accountId,
|
||||
meta,
|
||||
}),
|
||||
sendPairingReply: async (text) => {
|
||||
await interaction.reply({
|
||||
content: text,
|
||||
...replyOpts,
|
||||
});
|
||||
},
|
||||
});
|
||||
try {
|
||||
await interaction.reply({
|
||||
content: created
|
||||
? buildPairingReply({
|
||||
channel: "discord",
|
||||
idLine: `Your Discord user id: ${user.id}`,
|
||||
code,
|
||||
})
|
||||
: "Pairing already requested. Ask the bot owner to approve your code.",
|
||||
...replyOpts,
|
||||
});
|
||||
} catch {
|
||||
// Interaction may have expired
|
||||
if (!pairingResult.created) {
|
||||
try {
|
||||
await interaction.reply({
|
||||
content: "Pairing already requested. Ask the bot owner to approve your code.",
|
||||
...replyOpts,
|
||||
});
|
||||
} catch {
|
||||
// Interaction may have expired
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import type { DiscordDmCommandAccess } from "./dm-command-auth.js";
|
||||
|
||||
@@ -19,17 +20,25 @@ export async function handleDiscordDmCommandDecision(params: {
|
||||
|
||||
if (params.dmAccess.decision === "pairing") {
|
||||
const upsertPairingRequest = params.upsertPairingRequest ?? upsertChannelPairingRequest;
|
||||
const { code, created } = await upsertPairingRequest({
|
||||
const result = await issuePairingChallenge({
|
||||
channel: "discord",
|
||||
id: params.sender.id,
|
||||
accountId: params.accountId,
|
||||
senderId: params.sender.id,
|
||||
senderIdLine: `Your Discord user id: ${params.sender.id}`,
|
||||
meta: {
|
||||
tag: params.sender.tag,
|
||||
name: params.sender.name,
|
||||
},
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertPairingRequest({
|
||||
channel: "discord",
|
||||
id,
|
||||
accountId: params.accountId,
|
||||
meta,
|
||||
}),
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
if (created) {
|
||||
await params.onPairingCreated(code);
|
||||
if (result.created && result.code) {
|
||||
await params.onPairingCreated(result.code);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
resolveIMessageRemoteAttachmentRoots,
|
||||
} from "../../media/inbound-path-policy.js";
|
||||
import { kindFromMime } from "../../media/mime.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
@@ -288,36 +288,36 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
if (!sender) {
|
||||
return;
|
||||
}
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
await issuePairingChallenge({
|
||||
channel: "imessage",
|
||||
id: decision.senderId,
|
||||
accountId: accountInfo.accountId,
|
||||
senderId: decision.senderId,
|
||||
senderIdLine: `Your iMessage sender id: ${decision.senderId}`,
|
||||
meta: {
|
||||
sender: decision.senderId,
|
||||
chatId: chatId ? String(chatId) : undefined,
|
||||
},
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(`imessage pairing request sender=${decision.senderId}`);
|
||||
try {
|
||||
await sendMessageIMessage(
|
||||
sender,
|
||||
buildPairingReply({
|
||||
channel: "imessage",
|
||||
idLine: `Your iMessage sender id: ${decision.senderId}`,
|
||||
code,
|
||||
}),
|
||||
{
|
||||
client,
|
||||
maxBytes: mediaMaxBytes,
|
||||
accountId: accountInfo.accountId,
|
||||
...(chatId ? { chatId } : {}),
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "imessage",
|
||||
id,
|
||||
accountId: accountInfo.accountId,
|
||||
meta,
|
||||
}),
|
||||
onCreated: () => {
|
||||
logVerbose(`imessage pairing request sender=${decision.senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await sendMessageIMessage(sender, text, {
|
||||
client,
|
||||
maxBytes: mediaMaxBytes,
|
||||
accountId: accountInfo.accountId,
|
||||
...(chatId ? { chatId } : {}),
|
||||
});
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(`imessage pairing reply failed for ${decision.senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "../config/runtime-group-policy.js";
|
||||
import { danger, logVerbose } from "../globals.js";
|
||||
import { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
import { resolvePairingIdLabel } from "../pairing/pairing-labels.js";
|
||||
import { buildPairingReply } from "../pairing/pairing-messages.js";
|
||||
import {
|
||||
readChannelAllowFromStore,
|
||||
upsertChannelPairingRequest,
|
||||
@@ -237,15 +237,6 @@ async function sendLinePairingReply(params: {
|
||||
context: LineHandlerContext;
|
||||
}): Promise<void> {
|
||||
const { senderId, replyToken, context } = params;
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
channel: "line",
|
||||
id: senderId,
|
||||
accountId: context.account.accountId,
|
||||
});
|
||||
if (!created) {
|
||||
return;
|
||||
}
|
||||
logVerbose(`line pairing request sender=${senderId}`);
|
||||
const idLabel = (() => {
|
||||
try {
|
||||
return resolvePairingIdLabel("line");
|
||||
@@ -253,30 +244,42 @@ async function sendLinePairingReply(params: {
|
||||
return "lineUserId";
|
||||
}
|
||||
})();
|
||||
const text = buildPairingReply({
|
||||
await issuePairingChallenge({
|
||||
channel: "line",
|
||||
idLine: `Your ${idLabel}: ${senderId}`,
|
||||
code,
|
||||
});
|
||||
try {
|
||||
if (replyToken) {
|
||||
await replyMessageLine(replyToken, [{ type: "text", text }], {
|
||||
senderId,
|
||||
senderIdLine: `Your ${idLabel}: ${senderId}`,
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "line",
|
||||
id,
|
||||
accountId: context.account.accountId,
|
||||
channelAccessToken: context.account.channelAccessToken,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
try {
|
||||
await pushMessageLine(`line:${senderId}`, text, {
|
||||
accountId: context.account.accountId,
|
||||
channelAccessToken: context.account.channelAccessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
meta,
|
||||
}),
|
||||
onCreated: () => {
|
||||
logVerbose(`line pairing request sender=${senderId}`);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
if (replyToken) {
|
||||
try {
|
||||
await replyMessageLine(replyToken, [{ type: "text", text }], {
|
||||
accountId: context.account.accountId,
|
||||
channelAccessToken: context.account.channelAccessToken,
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await pushMessageLine(`line:${senderId}`, text, {
|
||||
accountId: context.account.accountId,
|
||||
channelAccessToken: context.account.channelAccessToken,
|
||||
});
|
||||
} catch (err) {
|
||||
logVerbose(`line pairing reply failed for ${senderId}: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function shouldProcessLineEvent(
|
||||
|
||||
90
src/pairing/pairing-challenge.test.ts
Normal file
90
src/pairing/pairing-challenge.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { issuePairingChallenge } from "./pairing-challenge.js";
|
||||
|
||||
describe("issuePairingChallenge", () => {
|
||||
it("creates and sends a pairing reply when request is newly created", async () => {
|
||||
const sent: string[] = [];
|
||||
|
||||
const result = await issuePairingChallenge({
|
||||
channel: "telegram",
|
||||
senderId: "123",
|
||||
senderIdLine: "Your Telegram user id: 123",
|
||||
upsertPairingRequest: async () => ({ code: "ABCD", created: true }),
|
||||
sendPairingReply: async (text) => {
|
||||
sent.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: true, code: "ABCD" });
|
||||
expect(sent).toHaveLength(1);
|
||||
expect(sent[0]).toContain("ABCD");
|
||||
});
|
||||
|
||||
it("does not send a reply when request already exists", async () => {
|
||||
const sendPairingReply = vi.fn(async () => {});
|
||||
|
||||
const result = await issuePairingChallenge({
|
||||
channel: "telegram",
|
||||
senderId: "123",
|
||||
senderIdLine: "Your Telegram user id: 123",
|
||||
upsertPairingRequest: async () => ({ code: "ABCD", created: false }),
|
||||
sendPairingReply,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: false });
|
||||
expect(sendPairingReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("supports custom reply text builder", async () => {
|
||||
const sent: string[] = [];
|
||||
|
||||
await issuePairingChallenge({
|
||||
channel: "line",
|
||||
senderId: "u1",
|
||||
senderIdLine: "Your line id: u1",
|
||||
upsertPairingRequest: async () => ({ code: "ZXCV", created: true }),
|
||||
buildReplyText: ({ code }) => `custom ${code}`,
|
||||
sendPairingReply: async (text) => {
|
||||
sent.push(text);
|
||||
},
|
||||
});
|
||||
|
||||
expect(sent).toEqual(["custom ZXCV"]);
|
||||
});
|
||||
|
||||
it("calls onCreated and forwards meta to upsert", async () => {
|
||||
const onCreated = vi.fn();
|
||||
const upsert = vi.fn(async () => ({ code: "1111", created: true }));
|
||||
|
||||
await issuePairingChallenge({
|
||||
channel: "discord",
|
||||
senderId: "42",
|
||||
senderIdLine: "Your Discord user id: 42",
|
||||
meta: { name: "alice" },
|
||||
upsertPairingRequest: upsert,
|
||||
onCreated,
|
||||
sendPairingReply: async () => {},
|
||||
});
|
||||
|
||||
expect(upsert).toHaveBeenCalledWith({ id: "42", meta: { name: "alice" } });
|
||||
expect(onCreated).toHaveBeenCalledWith({ code: "1111" });
|
||||
});
|
||||
|
||||
it("captures reply errors through onReplyError", async () => {
|
||||
const onReplyError = vi.fn();
|
||||
|
||||
const result = await issuePairingChallenge({
|
||||
channel: "signal",
|
||||
senderId: "+1555",
|
||||
senderIdLine: "Your Signal sender id: +1555",
|
||||
upsertPairingRequest: async () => ({ code: "9999", created: true }),
|
||||
sendPairingReply: async () => {
|
||||
throw new Error("send failed");
|
||||
},
|
||||
onReplyError,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ created: true, code: "9999" });
|
||||
expect(onReplyError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { isAllowedParsedChatSender } from "./allow-from.js";
|
||||
export { readBooleanParam } from "./boolean-param.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { resolveRequestUrl } from "./request-url.js";
|
||||
export {
|
||||
buildComputedAccountStatusSnapshot,
|
||||
|
||||
@@ -57,6 +57,7 @@ export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { buildAgentMediaPayload } from "./agent-media-payload.js";
|
||||
export { readJsonFileWithFallback } from "./json-store.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { createPersistentDedupe } from "./persistent-dedupe.js";
|
||||
export {
|
||||
buildBaseChannelStatusSummary,
|
||||
|
||||
@@ -63,6 +63,7 @@ export { formatDocsLink } from "../terminal/links.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { extractToolSend } from "./tool-send.js";
|
||||
export { resolveWebhookPath } from "./webhook-path.js";
|
||||
export type { WebhookInFlightLimiter } from "./webhook-request-guards.js";
|
||||
|
||||
@@ -60,6 +60,7 @@ export {
|
||||
export { formatDocsLink } from "../terminal/links.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { dispatchInboundReplyWithBase } from "./inbound-reply-dispatch.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
|
||||
@@ -84,6 +84,7 @@ export {
|
||||
resolveAccountWithDefaultFallback,
|
||||
} from "./account-resolution.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { createPersistentDedupe } from "./persistent-dedupe.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
|
||||
@@ -67,6 +67,7 @@ export { evaluateSenderGroupAccess } from "./group-access.js";
|
||||
export type { SenderGroupAccessDecision } from "./group-access.js";
|
||||
export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { buildChannelSendResult } from "./channel-send-result.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
|
||||
@@ -57,6 +57,7 @@ export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||
export { resolveChannelAccountConfigBasePath } from "./config-paths.js";
|
||||
export { loadOutboundMediaFromUrl } from "./outbound-media.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
export { buildChannelSendResult } from "./channel-send-result.js";
|
||||
export type { OutboundReplyPayload } from "./reply-payload.js";
|
||||
export {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Message } from "@grammyjs/types";
|
||||
import type { Bot } from "grammy";
|
||||
import type { DmPolicy } from "../config/types.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { buildPairingReply } from "../pairing/pairing-messages.js";
|
||||
import { issuePairingChallenge } from "../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../pairing/pairing-store.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
import { resolveSenderAllowMatch, type NormalizedAllowFrom } from "./bot-access.js";
|
||||
@@ -70,42 +70,46 @@ export async function enforceTelegramDmAccess(params: {
|
||||
if (dmPolicy === "pairing") {
|
||||
try {
|
||||
const telegramUserId = sender.userId ?? sender.candidateId;
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
await issuePairingChallenge({
|
||||
channel: "telegram",
|
||||
id: telegramUserId,
|
||||
accountId,
|
||||
senderId: telegramUserId,
|
||||
senderIdLine: `Your Telegram user id: ${telegramUserId}`,
|
||||
meta: {
|
||||
username: sender.username || undefined,
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
},
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "telegram",
|
||||
id,
|
||||
accountId,
|
||||
meta,
|
||||
}),
|
||||
onCreated: () => {
|
||||
logger.info(
|
||||
{
|
||||
chatId: String(chatId),
|
||||
senderUserId: sender.userId ?? undefined,
|
||||
username: sender.username || undefined,
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
matchKey: allowMatch.matchKey ?? "none",
|
||||
matchSource: allowMatch.matchSource ?? "none",
|
||||
},
|
||||
"telegram pairing request",
|
||||
);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
fn: () => bot.api.sendMessage(chatId, text),
|
||||
});
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
||||
},
|
||||
});
|
||||
if (created) {
|
||||
logger.info(
|
||||
{
|
||||
chatId: String(chatId),
|
||||
senderUserId: sender.userId ?? undefined,
|
||||
username: sender.username || undefined,
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
matchKey: allowMatch.matchKey ?? "none",
|
||||
matchSource: allowMatch.matchSource ?? "none",
|
||||
},
|
||||
"telegram pairing request",
|
||||
);
|
||||
await withTelegramApiErrorLogging({
|
||||
operation: "sendMessage",
|
||||
fn: () =>
|
||||
bot.api.sendMessage(
|
||||
chatId,
|
||||
buildPairingReply({
|
||||
channel: "telegram",
|
||||
idLine: `Your Telegram user id: ${telegramUserId}`,
|
||||
code,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(`telegram pairing reply failed for chat ${chatId}: ${String(err)}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
warnMissingProviderGroupPolicyFallbackOnce,
|
||||
} from "../../config/runtime-group-policy.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { buildPairingReply } from "../../pairing/pairing-messages.js";
|
||||
import { issuePairingChallenge } from "../../pairing/pairing-challenge.js";
|
||||
import { upsertChannelPairingRequest } from "../../pairing/pairing-store.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
@@ -171,28 +171,30 @@ export async function checkInboundAccessControl(params: {
|
||||
if (suppressPairingReply) {
|
||||
logVerbose(`Skipping pairing reply for historical DM from ${candidate}.`);
|
||||
} else {
|
||||
const { code, created } = await upsertChannelPairingRequest({
|
||||
await issuePairingChallenge({
|
||||
channel: "whatsapp",
|
||||
id: candidate,
|
||||
accountId: account.accountId,
|
||||
senderId: candidate,
|
||||
senderIdLine: `Your WhatsApp phone number: ${candidate}`,
|
||||
meta: { name: (params.pushName ?? "").trim() || undefined },
|
||||
});
|
||||
if (created) {
|
||||
logVerbose(
|
||||
`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`,
|
||||
);
|
||||
try {
|
||||
await params.sock.sendMessage(params.remoteJid, {
|
||||
text: buildPairingReply({
|
||||
channel: "whatsapp",
|
||||
idLine: `Your WhatsApp phone number: ${candidate}`,
|
||||
code,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
upsertPairingRequest: async ({ id, meta }) =>
|
||||
await upsertChannelPairingRequest({
|
||||
channel: "whatsapp",
|
||||
id,
|
||||
accountId: account.accountId,
|
||||
meta,
|
||||
}),
|
||||
onCreated: () => {
|
||||
logVerbose(
|
||||
`whatsapp pairing request sender=${candidate} name=${params.pushName ?? "unknown"}`,
|
||||
);
|
||||
},
|
||||
sendPairingReply: async (text) => {
|
||||
await params.sock.sendMessage(params.remoteJid, { text });
|
||||
},
|
||||
onReplyError: (err) => {
|
||||
logVerbose(`whatsapp pairing reply failed for ${candidate}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
|
||||
Reference in New Issue
Block a user