mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 10:02:04 +00:00
mattermost: fix DM media upload for unprefixed user IDs (#29925)
Merged via squash.
Prepared head SHA: 5cffcb072c
Co-authored-by: teconomix <6959299+teconomix@users.noreply.github.com>
Co-authored-by: mukhtharcm <56378562+mukhtharcm@users.noreply.github.com>
Reviewed-by: @mukhtharcm
This commit is contained in:
@@ -288,6 +288,18 @@ export type ChannelMessagingAdapter = {
|
||||
targetResolver?: {
|
||||
looksLikeId?: (raw: string, normalized?: string) => boolean;
|
||||
hint?: string;
|
||||
resolveTarget?: (params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
input: string;
|
||||
normalized: string;
|
||||
preferredKind?: ChannelDirectoryEntryKind | "channel";
|
||||
}) => Promise<{
|
||||
to: string;
|
||||
kind: ChannelDirectoryEntryKind | "channel";
|
||||
display?: string;
|
||||
source?: "normalized" | "directory";
|
||||
} | null>;
|
||||
};
|
||||
formatTargetDisplay?: (params: {
|
||||
target: string;
|
||||
|
||||
@@ -13,6 +13,10 @@ vi.mock("../../infra/outbound/channel-selection.js", () => ({
|
||||
.mockResolvedValue({ channel: "telegram", configured: ["telegram"] }),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/outbound/target-resolver.js", () => ({
|
||||
maybeResolveIdLikeTarget: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStoreSync: vi.fn(() => []),
|
||||
}));
|
||||
@@ -23,6 +27,7 @@ vi.mock("../../web/accounts.js", () => ({
|
||||
|
||||
import { loadSessionStore } from "../../config/sessions.js";
|
||||
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
|
||||
import { maybeResolveIdLikeTarget } from "../../infra/outbound/target-resolver.js";
|
||||
import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js";
|
||||
import { resolveWhatsAppAccount } from "../../web/accounts.js";
|
||||
import { resolveDeliveryTarget } from "./delivery-target.js";
|
||||
@@ -152,6 +157,30 @@ describe("resolveDeliveryTarget", () => {
|
||||
expect(result.accountId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("applies id-like target normalization before returning delivery targets", async () => {
|
||||
setMainSessionEntry(undefined);
|
||||
vi.mocked(maybeResolveIdLikeTarget).mockClear();
|
||||
vi.mocked(maybeResolveIdLikeTarget).mockResolvedValueOnce({
|
||||
to: "user:123456789",
|
||||
kind: "user",
|
||||
source: "directory",
|
||||
});
|
||||
|
||||
const result = await resolveDeliveryTarget(makeCfg({ bindings: [] }), AGENT_ID, {
|
||||
channel: "telegram",
|
||||
to: "123456789",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.to).toBe("user:123456789");
|
||||
expect(maybeResolveIdLikeTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
input: "123456789",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("selects correct binding when multiple agents have bindings", async () => {
|
||||
setMainSessionEntry(undefined);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolveStorePath,
|
||||
} from "../../config/sessions.js";
|
||||
import { resolveMessageChannelSelection } from "../../infra/outbound/channel-selection.js";
|
||||
import { maybeResolveIdLikeTarget } from "../../infra/outbound/target-resolver.js";
|
||||
import type { OutboundChannel } from "../../infra/outbound/targets.js";
|
||||
import {
|
||||
resolveOutboundTarget,
|
||||
@@ -190,10 +191,16 @@ export async function resolveDeliveryTarget(
|
||||
error: docked.error,
|
||||
};
|
||||
}
|
||||
const idLikeTarget = await maybeResolveIdLikeTarget({
|
||||
cfg,
|
||||
channel,
|
||||
input: docked.to,
|
||||
accountId,
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
channel,
|
||||
to: docked.to,
|
||||
to: idLikeTarget?.to ?? docked.to,
|
||||
accountId,
|
||||
threadId,
|
||||
mode,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../infra/outbound/outbound-session.js";
|
||||
import { normalizeReplyPayloadsForDelivery } from "../../infra/outbound/payloads.js";
|
||||
import { buildOutboundSessionContext } from "../../infra/outbound/session-context.js";
|
||||
import { maybeResolveIdLikeTarget } from "../../infra/outbound/target-resolver.js";
|
||||
import { resolveOutboundTarget } from "../../infra/outbound/targets.js";
|
||||
import { normalizePollInput } from "../../polls.js";
|
||||
import {
|
||||
@@ -194,6 +195,13 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
meta: { channel },
|
||||
};
|
||||
}
|
||||
const idLikeTarget = await maybeResolveIdLikeTarget({
|
||||
cfg,
|
||||
channel,
|
||||
input: resolved.to,
|
||||
accountId,
|
||||
});
|
||||
const deliveryTarget = idLikeTarget?.to ?? resolved.to;
|
||||
const outboundDeps = context.deps ? createOutboundSendDeps(context.deps) : undefined;
|
||||
const mirrorPayloads = normalizeReplyPayloadsForDelivery([
|
||||
{ text: message, mediaUrl, mediaUrls },
|
||||
@@ -225,7 +233,8 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
channel,
|
||||
agentId: effectiveAgentId,
|
||||
accountId,
|
||||
target: resolved.to,
|
||||
target: deliveryTarget,
|
||||
resolvedTarget: idLikeTarget,
|
||||
threadId,
|
||||
})
|
||||
: null;
|
||||
@@ -246,7 +255,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
const results = await deliverOutboundPayloads({
|
||||
cfg,
|
||||
channel: outboundChannel,
|
||||
to: resolved.to,
|
||||
to: deliveryTarget,
|
||||
accountId,
|
||||
payloads: [{ text: message, mediaUrl, mediaUrls }],
|
||||
session: outboundSession,
|
||||
|
||||
@@ -583,7 +583,12 @@ function resolveMattermostSession(
|
||||
}
|
||||
trimmed = trimmed.replace(/^mattermost:/i, "").trim();
|
||||
const lower = trimmed.toLowerCase();
|
||||
const isUser = lower.startsWith("user:") || trimmed.startsWith("@");
|
||||
const resolvedKind = params.resolvedTarget?.kind;
|
||||
const isUser =
|
||||
resolvedKind === "user" ||
|
||||
(resolvedKind !== "channel" &&
|
||||
resolvedKind !== "group" &&
|
||||
(lower.startsWith("user:") || trimmed.startsWith("@")));
|
||||
if (trimmed.startsWith("@")) {
|
||||
trimmed = trimmed.slice(1).trim();
|
||||
}
|
||||
|
||||
@@ -1142,6 +1142,28 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses resolved Mattermost user targets to route bare ids as DMs", async () => {
|
||||
const userId = "dthcxgoxhifn3pwh65cut3ud3w";
|
||||
const route = await resolveOutboundSessionRoute({
|
||||
cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig,
|
||||
channel: "mattermost",
|
||||
agentId: "main",
|
||||
target: userId,
|
||||
resolvedTarget: {
|
||||
to: `user:${userId}`,
|
||||
kind: "user",
|
||||
source: "directory",
|
||||
},
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
sessionKey: `agent:main:mattermost:direct:${userId}`,
|
||||
from: `mattermost:${userId}`,
|
||||
to: `user:${userId}`,
|
||||
chatType: "direct",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects bare numeric Discord targets when the caller has no kind hint", async () => {
|
||||
await expect(
|
||||
resolveOutboundSessionRoute({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { resetDirectoryCache, resolveMessagingTarget } from "./target-resolver.j
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listGroups: vi.fn(),
|
||||
listGroupsLive: vi.fn(),
|
||||
resolveTarget: vi.fn(),
|
||||
getChannelPlugin: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -20,6 +21,7 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
beforeEach(() => {
|
||||
mocks.listGroups.mockClear();
|
||||
mocks.listGroupsLive.mockClear();
|
||||
mocks.resolveTarget.mockClear();
|
||||
mocks.getChannelPlugin.mockClear();
|
||||
resetDirectoryCache();
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
@@ -27,6 +29,11 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
listGroups: mocks.listGroups,
|
||||
listGroupsLive: mocks.listGroupsLive,
|
||||
},
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
resolveTarget: mocks.resolveTarget,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,4 +82,43 @@ describe("resolveMessagingTarget (directory fallback)", () => {
|
||||
expect(mocks.listGroups).not.toHaveBeenCalled();
|
||||
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("lets plugins override id-like target resolution before falling back to raw ids", async () => {
|
||||
mocks.getChannelPlugin.mockReturnValue({
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: () => true,
|
||||
resolveTarget: mocks.resolveTarget,
|
||||
},
|
||||
},
|
||||
});
|
||||
mocks.resolveTarget.mockResolvedValue({
|
||||
to: "user:dm-user-id",
|
||||
kind: "user",
|
||||
source: "directory",
|
||||
});
|
||||
|
||||
const result = await resolveMessagingTarget({
|
||||
cfg,
|
||||
channel: "mattermost",
|
||||
input: "dthcxgoxhifn3pwh65cut3ud3w",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.target).toEqual({
|
||||
to: "user:dm-user-id",
|
||||
kind: "user",
|
||||
source: "directory",
|
||||
display: undefined,
|
||||
});
|
||||
}
|
||||
expect(mocks.resolveTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: "dthcxgoxhifn3pwh65cut3ud3w",
|
||||
}),
|
||||
);
|
||||
expect(mocks.listGroups).not.toHaveBeenCalled();
|
||||
expect(mocks.listGroupsLive).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,44 @@ export async function resolveChannelTarget(params: {
|
||||
return resolveMessagingTarget(params);
|
||||
}
|
||||
|
||||
export async function maybeResolveIdLikeTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChannelId;
|
||||
input: string;
|
||||
accountId?: string | null;
|
||||
preferredKind?: TargetResolveKind;
|
||||
}): Promise<ResolvedMessagingTarget | undefined> {
|
||||
const raw = normalizeChannelTargetInput(params.input);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const plugin = getChannelPlugin(params.channel);
|
||||
const resolver = plugin?.messaging?.targetResolver;
|
||||
if (!resolver?.resolveTarget) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
|
||||
if (resolver.looksLikeId && !resolver.looksLikeId(raw, normalized)) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = await resolver.resolveTarget({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
input: raw,
|
||||
normalized,
|
||||
preferredKind: params.preferredKind,
|
||||
});
|
||||
if (!resolved) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
to: resolved.to,
|
||||
kind: resolved.kind,
|
||||
display: resolved.display,
|
||||
source: resolved.source ?? "normalized",
|
||||
};
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
const directoryCache = new DirectoryCache<ChannelDirectoryEntry[]>(CACHE_TTL_MS);
|
||||
|
||||
@@ -388,6 +426,19 @@ export async function resolveMessagingTarget(params: {
|
||||
return false;
|
||||
};
|
||||
if (looksLikeTargetId()) {
|
||||
const resolvedIdLikeTarget = await maybeResolveIdLikeTarget({
|
||||
cfg: params.cfg,
|
||||
channel: params.channel,
|
||||
input: raw,
|
||||
accountId: params.accountId,
|
||||
preferredKind: params.preferredKind,
|
||||
});
|
||||
if (resolvedIdLikeTarget) {
|
||||
return {
|
||||
ok: true,
|
||||
target: resolvedIdLikeTarget,
|
||||
};
|
||||
}
|
||||
return buildNormalizedResolveResult({
|
||||
channel: params.channel,
|
||||
raw,
|
||||
|
||||
Reference in New Issue
Block a user