refactor(extensions): use scoped pairing helper
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
evictOldHistoryKeys,
|
||||
logAckFailure,
|
||||
@@ -421,6 +422,11 @@ export async function processMessage(
|
||||
target: WebhookTarget,
|
||||
): Promise<void> {
|
||||
const { account, config, runtime, core, statusSink } = target;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const privateApiEnabled = isBlueBubblesPrivateApiEnabled(account.accountId);
|
||||
|
||||
const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
|
||||
@@ -505,8 +511,9 @@ export async function processMessage(
|
||||
const configuredAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
const accessDecision = resolveDmGroupAccessWithLists({
|
||||
isGroup,
|
||||
@@ -587,8 +594,7 @@ export async function processMessage(
|
||||
}
|
||||
|
||||
if (accessDecision.decision === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "bluebubbles",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: message.senderId,
|
||||
meta: { name: message.senderName },
|
||||
});
|
||||
@@ -1381,6 +1387,11 @@ export async function processReaction(
|
||||
target: WebhookTarget,
|
||||
): Promise<void> {
|
||||
const { account, config, runtime, core } = target;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
if (reaction.fromMe) {
|
||||
return;
|
||||
}
|
||||
@@ -1389,8 +1400,9 @@ export async function processReaction(
|
||||
const groupPolicy = account.config.groupPolicy ?? "allowlist";
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: "bluebubbles",
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
const accessDecision = resolveDmGroupAccessWithLists({
|
||||
isGroup: reaction.isGroup,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
buildAgentMediaPayload,
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
createScopedPairingAccess,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
type HistoryEntry,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
@@ -675,6 +676,11 @@ export async function handleFeishuMessage(params: {
|
||||
|
||||
try {
|
||||
const core = getFeishuRuntime();
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "feishu",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
||||
ctx.content,
|
||||
cfg,
|
||||
@@ -683,7 +689,7 @@ export async function handleFeishuMessage(params: {
|
||||
!isGroup &&
|
||||
dmPolicy !== "allowlist" &&
|
||||
(dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
||||
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
|
||||
? await pairing.readAllowFromStore().catch(() => [])
|
||||
: [];
|
||||
const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
|
||||
const dmAllowed = resolveFeishuAllowlistMatch({
|
||||
@@ -695,8 +701,7 @@ export async function handleFeishuMessage(params: {
|
||||
|
||||
if (!isGroup && dmPolicy !== "open" && !dmAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "feishu",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: ctx.senderOpenId,
|
||||
meta: { name: ctx.senderName },
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
readJsonBodyWithLimit,
|
||||
registerWebhookTarget,
|
||||
@@ -396,6 +397,11 @@ async function processMessageWithPipeline(params: {
|
||||
mediaMaxMb: number;
|
||||
}): Promise<void> {
|
||||
const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "googlechat",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const space = event.space;
|
||||
const message = event.message;
|
||||
if (!space || !message) {
|
||||
@@ -514,7 +520,7 @@ async function processMessageWithPipeline(params: {
|
||||
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
|
||||
const storeAllowFrom =
|
||||
!isGroup && dmPolicy !== "allowlist" && (dmPolicy !== "open" || shouldComputeAuth)
|
||||
? await core.channel.pairing.readAllowFromStore("googlechat").catch(() => [])
|
||||
? await pairing.readAllowFromStore().catch(() => [])
|
||||
: [];
|
||||
const access = resolveDmGroupAccessWithLists({
|
||||
isGroup,
|
||||
@@ -590,8 +596,7 @@ async function processMessageWithPipeline(params: {
|
||||
|
||||
if (access.decision !== "allow") {
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "googlechat",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName || undefined, email: senderEmail },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
createScopedPairingAccess,
|
||||
createNormalizedOutboundDeliverer,
|
||||
createReplyPrefixOptions,
|
||||
formatTextWithAttachmentLinks,
|
||||
@@ -90,6 +91,11 @@ export async function handleIrcInbound(params: {
|
||||
}): Promise<void> {
|
||||
const { message, account, config, runtime, connectedNick, statusSink } = params;
|
||||
const core = getIrcRuntime();
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: CHANNEL_ID,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
const rawBody = message.text?.trim() ?? "";
|
||||
if (!rawBody) {
|
||||
@@ -123,8 +129,9 @@ export async function handleIrcInbound(params: {
|
||||
const configGroupAllowFrom = normalizeIrcAllowlist(account.config.groupAllowFrom);
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: CHANNEL_ID,
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
const storeAllowList = normalizeIrcAllowlist(storeAllowFrom);
|
||||
|
||||
@@ -202,8 +209,7 @@ export async function handleIrcInbound(params: {
|
||||
}).allowed;
|
||||
if (!dmAllowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: CHANNEL_ID,
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderDisplay.toLowerCase(),
|
||||
meta: { name: message.senderNick || undefined },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
createTypingCallbacks,
|
||||
formatAllowlistMatchMeta,
|
||||
@@ -98,6 +100,12 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
getMemberDisplayName,
|
||||
accountId,
|
||||
} = params;
|
||||
const resolvedAccountId = accountId?.trim() || DEFAULT_ACCOUNT_ID;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "matrix",
|
||||
accountId: resolvedAccountId,
|
||||
});
|
||||
|
||||
return async (roomId: string, event: MatrixRawEvent) => {
|
||||
try {
|
||||
@@ -229,8 +237,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
const storeAllowFrom = isDirectMessage
|
||||
? await readStoreAllowFromForDmPolicy({
|
||||
provider: "matrix",
|
||||
accountId: resolvedAccountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
})
|
||||
: [];
|
||||
const groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? [];
|
||||
@@ -270,8 +279,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
||||
});
|
||||
const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "matrix",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
import {
|
||||
buildAgentMediaPayload,
|
||||
DM_GROUP_ACCESS_REASON,
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
createTypingCallbacks,
|
||||
logInboundDrop,
|
||||
@@ -171,6 +172,11 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
cfg,
|
||||
accountId: opts.accountId,
|
||||
});
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "mattermost",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
||||
const botToken = opts.botToken?.trim() || account.botToken?.trim();
|
||||
if (!botToken) {
|
||||
@@ -362,8 +368,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
const storeAllowFrom = normalizeMattermostAllowList(
|
||||
await readStoreAllowFromForDmPolicy({
|
||||
provider: "mattermost",
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
}),
|
||||
);
|
||||
const accessDecision = resolveDmGroupAccessWithLists({
|
||||
@@ -424,8 +431,7 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
return;
|
||||
}
|
||||
if (accessDecision.decision === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "mattermost",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
@@ -862,8 +868,9 @@ export async function monitorMattermostProvider(opts: MonitorMattermostOpts = {}
|
||||
const storeAllowFrom = normalizeMattermostAllowList(
|
||||
await readStoreAllowFromForDmPolicy({
|
||||
provider: "mattermost",
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
}),
|
||||
);
|
||||
const reactionAccess = resolveDmGroupAccessWithLists({
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntriesIfEnabled,
|
||||
DEFAULT_GROUP_HISTORY_LIMIT,
|
||||
createScopedPairingAccess,
|
||||
logInboundDrop,
|
||||
recordPendingHistoryEntryIfEnabled,
|
||||
resolveControlCommandGate,
|
||||
@@ -57,6 +59,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
log,
|
||||
} = deps;
|
||||
const core = getMSTeamsRuntime();
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "msteams",
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
});
|
||||
const logVerboseMessage = (message: string) => {
|
||||
if (core.logging.shouldLogVerbose()) {
|
||||
log.debug?.(message);
|
||||
@@ -132,8 +139,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
|
||||
const storedAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: "msteams",
|
||||
accountId: pairing.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
|
||||
@@ -200,8 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
||||
});
|
||||
if (access.decision === "pairing") {
|
||||
const request = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "msteams",
|
||||
const request = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
GROUP_POLICY_BLOCKED_LABEL,
|
||||
createScopedPairingAccess,
|
||||
createNormalizedOutboundDeliverer,
|
||||
createReplyPrefixOptions,
|
||||
formatTextWithAttachmentLinks,
|
||||
@@ -58,6 +59,11 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
}): Promise<void> {
|
||||
const { message, account, config, runtime, statusSink } = params;
|
||||
const core = getNextcloudTalkRuntime();
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: CHANNEL_ID,
|
||||
accountId: account.accountId,
|
||||
});
|
||||
|
||||
const rawBody = message.text?.trim() ?? "";
|
||||
if (!rawBody) {
|
||||
@@ -99,8 +105,9 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom);
|
||||
const storeAllowFrom = await readStoreAllowFromForDmPolicy({
|
||||
provider: CHANNEL_ID,
|
||||
accountId: account.accountId,
|
||||
dmPolicy,
|
||||
readStore: (provider) => core.channel.pairing.readAllowFromStore(provider),
|
||||
readStore: pairing.readStoreForDmPolicy,
|
||||
});
|
||||
const storeAllowList = normalizeNextcloudTalkAllowlist(storeAllowFrom);
|
||||
|
||||
@@ -167,8 +174,7 @@ export async function handleNextcloudTalkInbound(params: {
|
||||
} else {
|
||||
if (access.decision !== "allow") {
|
||||
if (access.decision === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: CHANNEL_ID,
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName || undefined },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import type { MarkdownTableMode, OpenClawConfig, OutboundReplyPayload } from "openclaw/plugin-sdk";
|
||||
import {
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
resolveSenderCommandAuthorization,
|
||||
resolveOutboundMediaUrls,
|
||||
@@ -303,6 +304,11 @@ async function processMessageWithPipeline(params: {
|
||||
statusSink,
|
||||
fetcher,
|
||||
} = params;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "zalo",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
const { from, chat, message_id, date } = message;
|
||||
|
||||
const isGroup = chat.chat_type === "GROUP";
|
||||
@@ -358,7 +364,7 @@ async function processMessageWithPipeline(params: {
|
||||
configuredGroupAllowFrom: groupAllowFrom,
|
||||
senderId,
|
||||
isSenderAllowed: isZaloSenderAllowed,
|
||||
readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalo"),
|
||||
readAllowFromStore: pairing.readAllowFromStore,
|
||||
shouldComputeCommandAuthorized: (body, cfg) =>
|
||||
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
||||
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
||||
@@ -376,8 +382,7 @@ async function processMessageWithPipeline(params: {
|
||||
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "zalo",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName ?? undefined },
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
RuntimeEnv,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import {
|
||||
createScopedPairingAccess,
|
||||
createReplyPrefixOptions,
|
||||
resolveOutboundMediaUrls,
|
||||
mergeAllowlist,
|
||||
@@ -177,6 +178,11 @@ async function processMessage(
|
||||
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void,
|
||||
): Promise<void> {
|
||||
const { threadId, content, timestamp, metadata } = message;
|
||||
const pairing = createScopedPairingAccess({
|
||||
core,
|
||||
channel: "zalouser",
|
||||
accountId: account.accountId,
|
||||
});
|
||||
if (!content?.trim()) {
|
||||
return;
|
||||
}
|
||||
@@ -225,7 +231,7 @@ async function processMessage(
|
||||
configuredAllowFrom: configAllowFrom,
|
||||
senderId,
|
||||
isSenderAllowed,
|
||||
readAllowFromStore: () => core.channel.pairing.readAllowFromStore("zalouser"),
|
||||
readAllowFromStore: pairing.readAllowFromStore,
|
||||
shouldComputeCommandAuthorized: (body, cfg) =>
|
||||
core.channel.commands.shouldComputeCommandAuthorized(body, cfg),
|
||||
resolveCommandAuthorizedFromAuthorizers: (params) =>
|
||||
@@ -243,8 +249,7 @@ async function processMessage(
|
||||
|
||||
if (!allowed) {
|
||||
if (dmPolicy === "pairing") {
|
||||
const { code, created } = await core.channel.pairing.upsertPairingRequest({
|
||||
channel: "zalouser",
|
||||
const { code, created } = await pairing.upsertPairingRequest({
|
||||
id: senderId,
|
||||
meta: { name: senderName || undefined },
|
||||
});
|
||||
|
||||
@@ -216,6 +216,7 @@ export {
|
||||
type SenderGroupAccessReason,
|
||||
} from "./group-access.js";
|
||||
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||
export { createScopedPairingAccess } from "./pairing-access.js";
|
||||
export { handleSlackMessageAction } from "./slack-message-actions.js";
|
||||
export { extractToolSend } from "./tool-send.js";
|
||||
export {
|
||||
|
||||
36
src/plugin-sdk/pairing-access.ts
Normal file
36
src/plugin-sdk/pairing-access.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
type PairingApi = PluginRuntime["channel"]["pairing"];
|
||||
type ScopedUpsertInput = Omit<
|
||||
Parameters<PairingApi["upsertPairingRequest"]>[0],
|
||||
"channel" | "accountId"
|
||||
>;
|
||||
|
||||
export function createScopedPairingAccess(params: {
|
||||
core: PluginRuntime;
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
}) {
|
||||
const resolvedAccountId = normalizeAccountId(params.accountId);
|
||||
return {
|
||||
accountId: resolvedAccountId,
|
||||
readAllowFromStore: () =>
|
||||
params.core.channel.pairing.readAllowFromStore({
|
||||
channel: params.channel,
|
||||
accountId: resolvedAccountId,
|
||||
}),
|
||||
readStoreForDmPolicy: (provider: ChannelId, accountId: string) =>
|
||||
params.core.channel.pairing.readAllowFromStore({
|
||||
channel: provider,
|
||||
accountId: normalizeAccountId(accountId),
|
||||
}),
|
||||
upsertPairingRequest: (input: ScopedUpsertInput) =>
|
||||
params.core.channel.pairing.upsertPairingRequest({
|
||||
channel: params.channel,
|
||||
accountId: resolvedAccountId,
|
||||
...input,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user