@openclaw/mattermost 2026.7.1 → 2026.7.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts-C4Azn6rA.js +83 -0
- package/dist/api.js +3 -2
- package/dist/channel-plugin-api.js +3 -3
- package/dist/{channel-plugin-runtime-DhS8rwfs.js → channel-plugin-runtime-BXmjNn6B.js} +131 -167
- package/dist/channel-plugin-runtime.js +1 -1
- package/dist/{channel.runtime-D0SJSEei.js → channel.runtime-CKb2-z0p.js} +560 -296
- package/dist/{accounts-ITTlduDO.js → client-BzLj4dyf.js} +52 -102
- package/dist/doctor-contract-BaUvVh8e.js +19 -0
- package/dist/doctor-contract-api.js +1 -1
- package/dist/gateway-auth-api.js +1 -1
- package/dist/{gateway-auth-bypass-BIXLORHU.js → gateway-auth-bypass-CL1PxV3z.js} +3 -2
- package/dist/interactions-DO7J-ND1.js +375 -0
- package/dist/policy-api.js +1 -1
- package/dist/secret-contract-B4R5Bmhv.js +28 -0
- package/dist/secret-contract-api.js +1 -1
- package/dist/slash-route-api.js +1 -1
- package/dist/{slash-state-BfOSlkmn.js → slash-state-C9bpBN40.js} +124 -470
- package/npm-shrinkwrap.json +3 -3
- package/openclaw.plugin.json +209 -209
- package/package.json +4 -4
- package/dist/doctor-contract-ttH0DCuq.js +0 -7
- package/dist/secret-contract-Cx0LUNXy.js +0 -44
- package/dist/{monitor-auth-BiDuyvOc.js → monitor-auth-dEHa1_On.js} +3 -3
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { a as normalizeMattermostAllowList, b as readRequestBodyWithLimit, d as createChannelMessageReplyPipeline, g as loadOutboundMediaFromUrl, l as buildModelsProviderData, m as isTrustedProxyAddress, p as isRequestBodyLimitError, t as authorizeMattermostCommandInvocation, v as logTypingFailure, w as resolveClientIp } from "./monitor-auth-BiDuyvOc.js";
|
|
2
|
-
import { b as updateMattermostPost, c as createMattermostPost, d as fetchMattermostChannelByName, f as fetchMattermostMe, g as normalizeMattermostBaseUrl, h as fetchMattermostUserTeams, m as fetchMattermostUserByUsername, o as createMattermostClient, p as fetchMattermostUser, r as resolveMattermostAccount, s as createMattermostDirectChannelWithRetry, u as fetchMattermostChannel, v as resolveMattermostReplyDeliveryBarrierTimeoutMs, x as uploadMattermostFile, y as sendMattermostTyping } from "./accounts-ITTlduDO.js";
|
|
3
1
|
import { t as getMattermostRuntime } from "./runtime-CNB4YGqJ.js";
|
|
2
|
+
import { _ as uploadMattermostFile, c as fetchMattermostMe, d as fetchMattermostUserTeams, f as normalizeMattermostBaseUrl, h as sendMattermostTyping, i as createMattermostPost, l as fetchMattermostUser, m as resolveMattermostReplyDeliveryBarrierTimeoutMs, n as createMattermostClient, o as fetchMattermostChannel, r as createMattermostDirectChannelWithRetry, s as fetchMattermostChannelByName, u as fetchMattermostUserByUsername } from "./client-BzLj4dyf.js";
|
|
3
|
+
import { a as normalizeMattermostAllowList, b as readRequestBodyWithLimit, d as createChannelMessageReplyPipeline, g as loadOutboundMediaFromUrl, l as buildModelsProviderData, p as isRequestBodyLimitError, t as authorizeMattermostCommandInvocation, v as logTypingFailure } from "./monitor-auth-dEHa1_On.js";
|
|
4
|
+
import { c as setInteractionSecret, n as buildButtonProps, o as resolveInteractionCallbackUrl } from "./interactions-DO7J-ND1.js";
|
|
5
|
+
import { r as resolveMattermostAccount } from "./accounts-C4Azn6rA.js";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
4
8
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringifiedOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
9
|
+
import { asDateTimestampMs, parseStrictInteger, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
10
|
+
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
5
11
|
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
|
|
6
12
|
import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth-native";
|
|
7
13
|
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
|
8
|
-
import { asDateTimestampMs, parseStrictInteger, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
9
14
|
import { deliverTextOrMediaReply, isReasoningReplyPayload, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
10
|
-
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
11
15
|
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
|
|
12
16
|
import { getSessionEntry, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
|
|
13
17
|
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
|
14
18
|
import { Readable } from "node:stream";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
19
|
+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
20
|
+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
17
21
|
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
|
18
22
|
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
19
23
|
const MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES = 128;
|
|
@@ -369,7 +373,9 @@ const ACTION_IDS = {
|
|
|
369
373
|
function splitModelRef(modelRef) {
|
|
370
374
|
const match = normalizeOptionalString(modelRef)?.match(/^([^/]+)\/(.+)$/u);
|
|
371
375
|
if (!match) return null;
|
|
372
|
-
const
|
|
376
|
+
const rawProvider = match[1];
|
|
377
|
+
if (!rawProvider) return null;
|
|
378
|
+
const provider = normalizeProviderId(rawProvider);
|
|
373
379
|
const model = normalizeOptionalString(match[2]);
|
|
374
380
|
if (!provider || !model) return null;
|
|
375
381
|
return {
|
|
@@ -683,382 +689,84 @@ async function deliverMattermostReplyPayload(params) {
|
|
|
683
689
|
});
|
|
684
690
|
}
|
|
685
691
|
//#endregion
|
|
686
|
-
//#region extensions/mattermost/src/mattermost/
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
callbackUrls.set(accountId, url);
|
|
693
|
-
}
|
|
694
|
-
function resolveInteractionCallbackPath(accountId) {
|
|
695
|
-
return `/mattermost/interactions/${accountId}`;
|
|
696
|
-
}
|
|
697
|
-
function isWildcardBindHost(rawHost) {
|
|
698
|
-
const trimmed = rawHost.trim();
|
|
699
|
-
if (!trimmed) return false;
|
|
700
|
-
const host = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
701
|
-
return host === "0.0.0.0" || host === "::" || host === "0:0:0:0:0:0:0:0" || host === "::0";
|
|
702
|
-
}
|
|
703
|
-
function normalizeCallbackBaseUrl(baseUrl) {
|
|
704
|
-
return baseUrl.trim().replace(/\/+$/, "");
|
|
705
|
-
}
|
|
706
|
-
function headerValue(value) {
|
|
707
|
-
if (Array.isArray(value)) return normalizeOptionalString(value[0]);
|
|
708
|
-
return normalizeOptionalString(value);
|
|
709
|
-
}
|
|
710
|
-
function isAllowedInteractionSource(params) {
|
|
711
|
-
const { allowedSourceIps } = params;
|
|
712
|
-
if (!allowedSourceIps?.length) return true;
|
|
713
|
-
return isTrustedProxyAddress(resolveClientIp({
|
|
714
|
-
remoteAddr: params.req.socket?.remoteAddress,
|
|
715
|
-
forwardedFor: headerValue(params.req.headers["x-forwarded-for"]),
|
|
716
|
-
realIp: headerValue(params.req.headers["x-real-ip"]),
|
|
717
|
-
trustedProxies: params.trustedProxies,
|
|
718
|
-
allowRealIpFallback: params.allowRealIpFallback
|
|
719
|
-
}), allowedSourceIps);
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Resolve the interaction callback URL for an account.
|
|
723
|
-
* Falls back to computing it from interactions.callbackBaseUrl or gateway host config.
|
|
724
|
-
*/
|
|
725
|
-
function computeInteractionCallbackUrl(accountId, cfg) {
|
|
726
|
-
const path = resolveInteractionCallbackPath(accountId);
|
|
727
|
-
const callbackBaseUrl = normalizeOptionalString(cfg?.interactions?.callbackBaseUrl) ?? normalizeOptionalString(cfg?.channels?.mattermost?.interactions?.callbackBaseUrl);
|
|
728
|
-
if (callbackBaseUrl) return `${normalizeCallbackBaseUrl(callbackBaseUrl)}${path}`;
|
|
729
|
-
const port = typeof cfg?.gateway?.port === "number" ? cfg.gateway.port : 18789;
|
|
730
|
-
let host = cfg?.gateway?.customBindHost && !isWildcardBindHost(cfg.gateway.customBindHost) ? cfg.gateway.customBindHost.trim() : "localhost";
|
|
731
|
-
if (host.includes(":") && !(host.startsWith("[") && host.endsWith("]"))) host = `[${host}]`;
|
|
732
|
-
return `http://${host}:${port}${path}`;
|
|
692
|
+
//#region extensions/mattermost/src/mattermost/target-resolution.ts
|
|
693
|
+
const MATTERMOST_OPAQUE_TARGET_CACHE_MAX_ENTRIES = 1024;
|
|
694
|
+
const mattermostOpaqueTargetCache = /* @__PURE__ */ new Map();
|
|
695
|
+
function cacheMattermostOpaqueTarget(key, kind) {
|
|
696
|
+
mattermostOpaqueTargetCache.set(key, kind);
|
|
697
|
+
pruneMapToMaxSize(mattermostOpaqueTargetCache, MATTERMOST_OPAQUE_TARGET_CACHE_MAX_ENTRIES);
|
|
733
698
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
* Prefers the in-memory registered URL (set by the gateway monitor) so callers outside the
|
|
737
|
-
* monitor lifecycle can reuse the runtime-validated callback destination.
|
|
738
|
-
*/
|
|
739
|
-
function resolveInteractionCallbackUrl(accountId, cfg) {
|
|
740
|
-
const cached = callbackUrls.get(accountId);
|
|
741
|
-
if (cached) return cached;
|
|
742
|
-
return computeInteractionCallbackUrl(accountId, cfg);
|
|
699
|
+
function cacheKey$1(baseUrl, token, id) {
|
|
700
|
+
return `${baseUrl}::${token}::${id}`;
|
|
743
701
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return createHmac("sha256", "openclaw-mattermost-interactions").update(botToken).digest("hex");
|
|
702
|
+
/** Mattermost IDs are 26-character lowercase alphanumeric strings. */
|
|
703
|
+
function isMattermostId(value) {
|
|
704
|
+
return /^[a-z0-9]{26}$/.test(value);
|
|
748
705
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
706
|
+
function parseMattermostTarget(raw) {
|
|
707
|
+
const trimmed = raw.trim();
|
|
708
|
+
if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
|
|
709
|
+
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
|
710
|
+
if (lower.startsWith("channel:")) {
|
|
711
|
+
const id = trimmed.slice(8).trim();
|
|
712
|
+
if (!id) throw new Error("Channel id is required for Mattermost sends");
|
|
713
|
+
if (id.startsWith("#")) {
|
|
714
|
+
const name = id.slice(1).trim();
|
|
715
|
+
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
716
|
+
return {
|
|
717
|
+
kind: "channel-name",
|
|
718
|
+
name
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (!isMattermostId(id)) return {
|
|
722
|
+
kind: "channel-name",
|
|
723
|
+
name: id
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
kind: "channel",
|
|
727
|
+
id
|
|
728
|
+
};
|
|
753
729
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const first = interactionSecrets.values().next().value;
|
|
762
|
-
if (typeof first === "string") return first;
|
|
730
|
+
if (lower.startsWith("user:")) {
|
|
731
|
+
const id = trimmed.slice(5).trim();
|
|
732
|
+
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
733
|
+
return {
|
|
734
|
+
kind: "user",
|
|
735
|
+
id
|
|
736
|
+
};
|
|
763
737
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
738
|
+
if (lower.startsWith("mattermost:")) {
|
|
739
|
+
const id = trimmed.slice(11).trim();
|
|
740
|
+
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
741
|
+
return {
|
|
742
|
+
kind: "user",
|
|
743
|
+
id
|
|
744
|
+
};
|
|
771
745
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
function verifyInteractionToken(context, token, accountId) {
|
|
780
|
-
return safeEqualSecret(generateInteractionToken(context, accountId), token);
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Build Mattermost `props.attachments` with interactive buttons.
|
|
784
|
-
*
|
|
785
|
-
* Each button includes an HMAC token in its integration context so the
|
|
786
|
-
* callback handler can verify the request originated from a legitimate
|
|
787
|
-
* button click (Mattermost's recommended security pattern).
|
|
788
|
-
*/
|
|
789
|
-
/**
|
|
790
|
-
* Sanitize a button ID so Mattermost's action router can match it.
|
|
791
|
-
* Mattermost uses the action ID in the URL path `/api/v4/posts/{id}/actions/{actionId}`
|
|
792
|
-
* and IDs containing hyphens or underscores break the server-side routing.
|
|
793
|
-
* See: https://github.com/mattermost/mattermost/issues/25747
|
|
794
|
-
*/
|
|
795
|
-
function sanitizeActionId(id) {
|
|
796
|
-
return id.replace(/[-_]/g, "");
|
|
797
|
-
}
|
|
798
|
-
function buildButtonAttachments(params) {
|
|
799
|
-
const actions = params.buttons.map((btn) => {
|
|
800
|
-
const safeId = sanitizeActionId(btn.id);
|
|
801
|
-
const context = {
|
|
802
|
-
action_id: safeId,
|
|
803
|
-
...btn.context
|
|
746
|
+
if (trimmed.startsWith("@")) {
|
|
747
|
+
const username = trimmed.slice(1).trim();
|
|
748
|
+
if (!username) throw new Error("Username is required for Mattermost sends");
|
|
749
|
+
return {
|
|
750
|
+
kind: "user",
|
|
751
|
+
username
|
|
804
752
|
};
|
|
805
|
-
|
|
753
|
+
}
|
|
754
|
+
if (trimmed.startsWith("#")) {
|
|
755
|
+
const name = trimmed.slice(1).trim();
|
|
756
|
+
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
806
757
|
return {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
name: btn.name,
|
|
810
|
-
style: btn.style,
|
|
811
|
-
integration: {
|
|
812
|
-
url: params.callbackUrl,
|
|
813
|
-
context: {
|
|
814
|
-
...context,
|
|
815
|
-
_token: token
|
|
816
|
-
}
|
|
817
|
-
}
|
|
758
|
+
kind: "channel-name",
|
|
759
|
+
name
|
|
818
760
|
};
|
|
819
|
-
});
|
|
820
|
-
return [{
|
|
821
|
-
text: params.text ?? "",
|
|
822
|
-
actions
|
|
823
|
-
}];
|
|
824
|
-
}
|
|
825
|
-
function buildButtonProps(params) {
|
|
826
|
-
const buttons = params.buttons.flatMap((item) => Array.isArray(item) ? item : [item]).map((btn) => ({
|
|
827
|
-
id: normalizeStringifiedOptionalString(btn.id ?? btn.callback_data) ?? "",
|
|
828
|
-
name: normalizeStringifiedOptionalString(btn.text ?? btn.name ?? btn.label) ?? "",
|
|
829
|
-
style: btn.style ?? "default",
|
|
830
|
-
context: typeof btn.context === "object" && btn.context !== null ? {
|
|
831
|
-
...btn.context,
|
|
832
|
-
[SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId
|
|
833
|
-
} : { [SIGNED_CHANNEL_ID_CONTEXT_KEY]: params.channelId }
|
|
834
|
-
})).filter((btn) => btn.id && btn.name);
|
|
835
|
-
if (buttons.length === 0) return;
|
|
836
|
-
return { attachments: buildButtonAttachments({
|
|
837
|
-
callbackUrl: params.callbackUrl,
|
|
838
|
-
accountId: params.accountId,
|
|
839
|
-
buttons,
|
|
840
|
-
text: params.text
|
|
841
|
-
}) };
|
|
842
|
-
}
|
|
843
|
-
function readInteractionBody(req) {
|
|
844
|
-
return readRequestBodyWithLimit(req, {
|
|
845
|
-
maxBytes: INTERACTION_MAX_BODY_BYTES,
|
|
846
|
-
timeoutMs: INTERACTION_BODY_TIMEOUT_MS
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
function createMattermostInteractionHandler(params) {
|
|
850
|
-
const { client, accountId, log } = params;
|
|
851
|
-
const core = getMattermostRuntime();
|
|
852
|
-
function parseInteractionPayload(raw) {
|
|
853
|
-
try {
|
|
854
|
-
return JSON.parse(raw);
|
|
855
|
-
} catch {
|
|
856
|
-
throw new Error("Mattermost interaction body was malformed JSON");
|
|
857
|
-
}
|
|
858
761
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
}
|
|
867
|
-
if (!isAllowedInteractionSource({
|
|
868
|
-
req,
|
|
869
|
-
allowedSourceIps: params.allowedSourceIps,
|
|
870
|
-
trustedProxies: params.trustedProxies,
|
|
871
|
-
allowRealIpFallback: params.allowRealIpFallback
|
|
872
|
-
})) {
|
|
873
|
-
log?.(`mattermost interaction: rejected callback source remote=${req.socket?.remoteAddress ?? "?"}`);
|
|
874
|
-
res.statusCode = 403;
|
|
875
|
-
res.setHeader("Content-Type", "application/json");
|
|
876
|
-
res.end(JSON.stringify({ error: "Forbidden origin" }));
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
let payload;
|
|
880
|
-
try {
|
|
881
|
-
payload = parseInteractionPayload(await readInteractionBody(req));
|
|
882
|
-
} catch (err) {
|
|
883
|
-
log?.(`mattermost interaction: failed to parse body: ${String(err)}`);
|
|
884
|
-
res.statusCode = 400;
|
|
885
|
-
res.setHeader("Content-Type", "application/json");
|
|
886
|
-
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
const context = payload.context;
|
|
890
|
-
if (!context) {
|
|
891
|
-
res.statusCode = 400;
|
|
892
|
-
res.setHeader("Content-Type", "application/json");
|
|
893
|
-
res.end(JSON.stringify({ error: "Missing context" }));
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
const token = context["_token"];
|
|
897
|
-
if (typeof token !== "string") {
|
|
898
|
-
log?.("mattermost interaction: missing _token in context");
|
|
899
|
-
res.statusCode = 403;
|
|
900
|
-
res.setHeader("Content-Type", "application/json");
|
|
901
|
-
res.end(JSON.stringify({ error: "Missing token" }));
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
const { _token, ...contextWithoutToken } = context;
|
|
905
|
-
if (!verifyInteractionToken(contextWithoutToken, token, accountId)) {
|
|
906
|
-
log?.("mattermost interaction: invalid _token");
|
|
907
|
-
res.statusCode = 403;
|
|
908
|
-
res.setHeader("Content-Type", "application/json");
|
|
909
|
-
res.end(JSON.stringify({ error: "Invalid token" }));
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
const actionId = context.action_id;
|
|
913
|
-
if (typeof actionId !== "string") {
|
|
914
|
-
res.statusCode = 400;
|
|
915
|
-
res.setHeader("Content-Type", "application/json");
|
|
916
|
-
res.end(JSON.stringify({ error: "Missing action_id in context" }));
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
const signedChannelId = typeof contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY] === "string" ? contextWithoutToken[SIGNED_CHANNEL_ID_CONTEXT_KEY].trim() : "";
|
|
920
|
-
if (signedChannelId && signedChannelId !== payload.channel_id) {
|
|
921
|
-
log?.(`mattermost interaction: signed channel mismatch payload=${payload.channel_id} signed=${signedChannelId}`);
|
|
922
|
-
res.statusCode = 403;
|
|
923
|
-
res.setHeader("Content-Type", "application/json");
|
|
924
|
-
res.end(JSON.stringify({ error: "Channel mismatch" }));
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
const userName = payload.user_name ?? payload.user_id;
|
|
928
|
-
let originalMessage;
|
|
929
|
-
let originalPost;
|
|
930
|
-
let clickedButtonName = null;
|
|
931
|
-
try {
|
|
932
|
-
originalPost = await client.request(`/posts/${payload.post_id}`);
|
|
933
|
-
const postChannelId = originalPost.channel_id?.trim();
|
|
934
|
-
if (!postChannelId || postChannelId !== payload.channel_id) {
|
|
935
|
-
log?.(`mattermost interaction: post channel mismatch payload=${payload.channel_id} post=${postChannelId ?? "<missing>"}`);
|
|
936
|
-
res.statusCode = 403;
|
|
937
|
-
res.setHeader("Content-Type", "application/json");
|
|
938
|
-
res.end(JSON.stringify({ error: "Post/channel mismatch" }));
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
originalMessage = originalPost.message ?? "";
|
|
942
|
-
const postAttachments = Array.isArray(originalPost?.props?.attachments) ? originalPost.props.attachments : [];
|
|
943
|
-
for (const att of postAttachments) {
|
|
944
|
-
const match = att.actions?.find((a) => a.id === actionId);
|
|
945
|
-
if (match?.name) {
|
|
946
|
-
clickedButtonName = match.name;
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
if (clickedButtonName === null) {
|
|
951
|
-
log?.(`mattermost interaction: action ${actionId} not found in post ${payload.post_id}`);
|
|
952
|
-
res.statusCode = 403;
|
|
953
|
-
res.setHeader("Content-Type", "application/json");
|
|
954
|
-
res.end(JSON.stringify({ error: "Unknown action" }));
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
} catch (err) {
|
|
958
|
-
log?.(`mattermost interaction: failed to validate post ${payload.post_id}: ${String(err)}`);
|
|
959
|
-
res.statusCode = 500;
|
|
960
|
-
res.setHeader("Content-Type", "application/json");
|
|
961
|
-
res.end(JSON.stringify({ error: "Failed to validate interaction" }));
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
if (!originalPost) {
|
|
965
|
-
log?.(`mattermost interaction: missing fetched post ${payload.post_id}`);
|
|
966
|
-
res.statusCode = 500;
|
|
967
|
-
res.setHeader("Content-Type", "application/json");
|
|
968
|
-
res.end(JSON.stringify({ error: "Failed to load interaction post" }));
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
log?.(`mattermost interaction: action=${actionId} user=${payload.user_name ?? payload.user_id} post=${payload.post_id} channel=${payload.channel_id}`);
|
|
972
|
-
if (params.authorizeButtonClick) try {
|
|
973
|
-
const authorization = await params.authorizeButtonClick({
|
|
974
|
-
payload,
|
|
975
|
-
post: originalPost
|
|
976
|
-
});
|
|
977
|
-
if (!authorization.ok) {
|
|
978
|
-
res.statusCode = authorization.statusCode ?? 200;
|
|
979
|
-
res.setHeader("Content-Type", "application/json");
|
|
980
|
-
res.end(JSON.stringify(authorization.response ?? { ephemeral_text: "You are not allowed to use this action here." }));
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
} catch (err) {
|
|
984
|
-
log?.(`mattermost interaction: authorization failed: ${String(err)}`);
|
|
985
|
-
res.statusCode = 500;
|
|
986
|
-
res.setHeader("Content-Type", "application/json");
|
|
987
|
-
res.end(JSON.stringify({ error: "Interaction authorization failed" }));
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
if (params.handleInteraction) try {
|
|
991
|
-
const response = await params.handleInteraction({
|
|
992
|
-
payload,
|
|
993
|
-
userName,
|
|
994
|
-
actionId,
|
|
995
|
-
actionName: clickedButtonName,
|
|
996
|
-
originalMessage,
|
|
997
|
-
context: contextWithoutToken,
|
|
998
|
-
post: originalPost
|
|
999
|
-
});
|
|
1000
|
-
if (response !== null) {
|
|
1001
|
-
res.statusCode = 200;
|
|
1002
|
-
res.setHeader("Content-Type", "application/json");
|
|
1003
|
-
res.end(JSON.stringify(response));
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
} catch (err) {
|
|
1007
|
-
log?.(`mattermost interaction: custom handler failed: ${String(err)}`);
|
|
1008
|
-
res.statusCode = 500;
|
|
1009
|
-
res.setHeader("Content-Type", "application/json");
|
|
1010
|
-
res.end(JSON.stringify({ error: "Interaction handler failed" }));
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
try {
|
|
1014
|
-
const eventLabel = `Mattermost button click: action="${actionId}" by ${payload.user_name ?? payload.user_id} in channel ${payload.channel_id}`;
|
|
1015
|
-
const sessionKey = params.resolveSessionKey ? await params.resolveSessionKey({
|
|
1016
|
-
channelId: payload.channel_id,
|
|
1017
|
-
userId: payload.user_id,
|
|
1018
|
-
post: originalPost
|
|
1019
|
-
}) : `agent:main:mattermost:${accountId}:${payload.channel_id}`;
|
|
1020
|
-
core.system.enqueueSystemEvent(eventLabel, {
|
|
1021
|
-
sessionKey,
|
|
1022
|
-
contextKey: `mattermost:interaction:${payload.post_id}:${actionId}`
|
|
1023
|
-
});
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
log?.(`mattermost interaction: system event dispatch failed: ${String(err)}`);
|
|
1026
|
-
}
|
|
1027
|
-
try {
|
|
1028
|
-
await updateMattermostPost(client, payload.post_id, {
|
|
1029
|
-
message: originalMessage,
|
|
1030
|
-
props: { attachments: [{ text: `✓ **${clickedButtonName}** selected by @${userName}` }] }
|
|
1031
|
-
});
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
log?.(`mattermost interaction: failed to update post ${payload.post_id}: ${String(err)}`);
|
|
1034
|
-
}
|
|
1035
|
-
res.statusCode = 200;
|
|
1036
|
-
res.setHeader("Content-Type", "application/json");
|
|
1037
|
-
res.end("{}");
|
|
1038
|
-
if (params.dispatchButtonClick) try {
|
|
1039
|
-
await params.dispatchButtonClick({
|
|
1040
|
-
channelId: payload.channel_id,
|
|
1041
|
-
userId: payload.user_id,
|
|
1042
|
-
userName,
|
|
1043
|
-
actionId,
|
|
1044
|
-
actionName: clickedButtonName,
|
|
1045
|
-
postId: payload.post_id,
|
|
1046
|
-
post: originalPost
|
|
1047
|
-
});
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
log?.(`mattermost interaction: dispatchButtonClick failed: ${String(err)}`);
|
|
1050
|
-
}
|
|
762
|
+
if (!isMattermostId(trimmed)) return {
|
|
763
|
+
kind: "channel-name",
|
|
764
|
+
name: trimmed
|
|
765
|
+
};
|
|
766
|
+
return {
|
|
767
|
+
kind: "channel",
|
|
768
|
+
id: trimmed
|
|
1051
769
|
};
|
|
1052
|
-
}
|
|
1053
|
-
//#endregion
|
|
1054
|
-
//#region extensions/mattermost/src/mattermost/target-resolution.ts
|
|
1055
|
-
const mattermostOpaqueTargetCache = /* @__PURE__ */ new Map();
|
|
1056
|
-
function cacheKey$1(baseUrl, token, id) {
|
|
1057
|
-
return `${baseUrl}::${token}::${id}`;
|
|
1058
|
-
}
|
|
1059
|
-
/** Mattermost IDs are 26-character lowercase alphanumeric strings. */
|
|
1060
|
-
function isMattermostId(value) {
|
|
1061
|
-
return /^[a-z0-9]{26}$/.test(value);
|
|
1062
770
|
}
|
|
1063
771
|
function isExplicitMattermostTarget(raw) {
|
|
1064
772
|
const trimmed = raw.trim();
|
|
@@ -1080,20 +788,16 @@ async function resolveMattermostOpaqueTarget(params) {
|
|
|
1080
788
|
cfg: params.cfg,
|
|
1081
789
|
accountId: params.accountId
|
|
1082
790
|
}) : null;
|
|
791
|
+
if (account && !account.enabled) throw new Error(`Mattermost account "${account.accountId}" is disabled`);
|
|
1083
792
|
const token = normalizeOptionalString(params.token) ?? normalizeOptionalString(account?.botToken);
|
|
1084
793
|
const baseUrl = normalizeMattermostBaseUrl(params.baseUrl ?? account?.baseUrl);
|
|
1085
794
|
if (!token || !baseUrl) return null;
|
|
1086
795
|
const key = cacheKey$1(baseUrl, token, input);
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1089
|
-
kind:
|
|
796
|
+
const cachedKind = mattermostOpaqueTargetCache.get(key);
|
|
797
|
+
if (cachedKind) return {
|
|
798
|
+
kind: cachedKind,
|
|
1090
799
|
id: input,
|
|
1091
|
-
to:
|
|
1092
|
-
};
|
|
1093
|
-
if (cached === false) return {
|
|
1094
|
-
kind: "channel",
|
|
1095
|
-
id: input,
|
|
1096
|
-
to: `channel:${input}`
|
|
800
|
+
to: `${cachedKind}:${input}`
|
|
1097
801
|
};
|
|
1098
802
|
const client = createMattermostClient({
|
|
1099
803
|
baseUrl,
|
|
@@ -1102,14 +806,14 @@ async function resolveMattermostOpaqueTarget(params) {
|
|
|
1102
806
|
});
|
|
1103
807
|
try {
|
|
1104
808
|
await fetchMattermostUser(client, input);
|
|
1105
|
-
|
|
809
|
+
cacheMattermostOpaqueTarget(key, "user");
|
|
1106
810
|
return {
|
|
1107
811
|
kind: "user",
|
|
1108
812
|
id: input,
|
|
1109
813
|
to: `user:${input}`
|
|
1110
814
|
};
|
|
1111
815
|
} catch (err) {
|
|
1112
|
-
if (parseMattermostApiStatus(err) === 404)
|
|
816
|
+
if (parseMattermostApiStatus(err) === 404) cacheMattermostOpaqueTarget(key, "channel");
|
|
1113
817
|
return {
|
|
1114
818
|
kind: "channel",
|
|
1115
819
|
id: input,
|
|
@@ -1119,10 +823,17 @@ async function resolveMattermostOpaqueTarget(params) {
|
|
|
1119
823
|
}
|
|
1120
824
|
//#endregion
|
|
1121
825
|
//#region extensions/mattermost/src/mattermost/send.ts
|
|
826
|
+
const MATTERMOST_BOT_USER_CACHE_MAX_ENTRIES = 64;
|
|
827
|
+
const MATTERMOST_TARGET_CACHE_MAX_ENTRIES = 1024;
|
|
1122
828
|
const botUserCache = /* @__PURE__ */ new Map();
|
|
1123
829
|
const userByNameCache = /* @__PURE__ */ new Map();
|
|
1124
830
|
const channelByNameCache = /* @__PURE__ */ new Map();
|
|
1125
831
|
const dmChannelCache = /* @__PURE__ */ new Map();
|
|
832
|
+
function cacheOutboundEntry(cache, key, value, maxEntries) {
|
|
833
|
+
cache.delete(key);
|
|
834
|
+
cache.set(key, value);
|
|
835
|
+
pruneMapToMaxSize(cache, maxEntries);
|
|
836
|
+
}
|
|
1126
837
|
const getCore = () => getMattermostRuntime();
|
|
1127
838
|
function createMattermostSendReceipt(params) {
|
|
1128
839
|
const messageIds = params.messageId.trim() && params.messageId !== "unknown" ? [params.messageId] : [];
|
|
@@ -1161,71 +872,6 @@ function normalizeMessage(text, mediaUrl) {
|
|
|
1161
872
|
function isHttpUrl(value) {
|
|
1162
873
|
return /^https?:\/\//i.test(value);
|
|
1163
874
|
}
|
|
1164
|
-
function parseMattermostTarget(raw) {
|
|
1165
|
-
const trimmed = raw.trim();
|
|
1166
|
-
if (!trimmed) throw new Error("Recipient is required for Mattermost sends");
|
|
1167
|
-
const lower = normalizeLowercaseStringOrEmpty(trimmed);
|
|
1168
|
-
if (lower.startsWith("channel:")) {
|
|
1169
|
-
const id = trimmed.slice(8).trim();
|
|
1170
|
-
if (!id) throw new Error("Channel id is required for Mattermost sends");
|
|
1171
|
-
if (id.startsWith("#")) {
|
|
1172
|
-
const name = id.slice(1).trim();
|
|
1173
|
-
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
1174
|
-
return {
|
|
1175
|
-
kind: "channel-name",
|
|
1176
|
-
name
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
if (!isMattermostId(id)) return {
|
|
1180
|
-
kind: "channel-name",
|
|
1181
|
-
name: id
|
|
1182
|
-
};
|
|
1183
|
-
return {
|
|
1184
|
-
kind: "channel",
|
|
1185
|
-
id
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
if (lower.startsWith("user:")) {
|
|
1189
|
-
const id = trimmed.slice(5).trim();
|
|
1190
|
-
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
1191
|
-
return {
|
|
1192
|
-
kind: "user",
|
|
1193
|
-
id
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
if (lower.startsWith("mattermost:")) {
|
|
1197
|
-
const id = trimmed.slice(11).trim();
|
|
1198
|
-
if (!id) throw new Error("User id is required for Mattermost sends");
|
|
1199
|
-
return {
|
|
1200
|
-
kind: "user",
|
|
1201
|
-
id
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
if (trimmed.startsWith("@")) {
|
|
1205
|
-
const username = trimmed.slice(1).trim();
|
|
1206
|
-
if (!username) throw new Error("Username is required for Mattermost sends");
|
|
1207
|
-
return {
|
|
1208
|
-
kind: "user",
|
|
1209
|
-
username
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
if (trimmed.startsWith("#")) {
|
|
1213
|
-
const name = trimmed.slice(1).trim();
|
|
1214
|
-
if (!name) throw new Error("Channel name is required for Mattermost sends");
|
|
1215
|
-
return {
|
|
1216
|
-
kind: "channel-name",
|
|
1217
|
-
name
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
if (!isMattermostId(trimmed)) return {
|
|
1221
|
-
kind: "channel-name",
|
|
1222
|
-
name: trimmed
|
|
1223
|
-
};
|
|
1224
|
-
return {
|
|
1225
|
-
kind: "channel",
|
|
1226
|
-
id: trimmed
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
875
|
async function resolveBotUser(baseUrl, token, allowPrivateNetwork) {
|
|
1230
876
|
const key = cacheKey(baseUrl, token);
|
|
1231
877
|
const cached = botUserCache.get(key);
|
|
@@ -1235,7 +881,7 @@ async function resolveBotUser(baseUrl, token, allowPrivateNetwork) {
|
|
|
1235
881
|
botToken: token,
|
|
1236
882
|
allowPrivateNetwork
|
|
1237
883
|
}));
|
|
1238
|
-
botUserCache
|
|
884
|
+
cacheOutboundEntry(botUserCache, key, user, MATTERMOST_BOT_USER_CACHE_MAX_ENTRIES);
|
|
1239
885
|
return user;
|
|
1240
886
|
}
|
|
1241
887
|
async function resolveUserIdByUsername(params) {
|
|
@@ -1248,7 +894,7 @@ async function resolveUserIdByUsername(params) {
|
|
|
1248
894
|
botToken: token,
|
|
1249
895
|
allowPrivateNetwork: params.allowPrivateNetwork
|
|
1250
896
|
}), username);
|
|
1251
|
-
userByNameCache
|
|
897
|
+
cacheOutboundEntry(userByNameCache, key, user, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
|
|
1252
898
|
return user.id;
|
|
1253
899
|
}
|
|
1254
900
|
async function resolveChannelIdByName(params) {
|
|
@@ -1265,7 +911,7 @@ async function resolveChannelIdByName(params) {
|
|
|
1265
911
|
for (const team of teams) try {
|
|
1266
912
|
const channel = await fetchMattermostChannelByName(client, team.id, name);
|
|
1267
913
|
if (channel?.id) {
|
|
1268
|
-
channelByNameCache
|
|
914
|
+
cacheOutboundEntry(channelByNameCache, key, channel.id, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
|
|
1269
915
|
return channel.id;
|
|
1270
916
|
}
|
|
1271
917
|
} catch {}
|
|
@@ -1313,7 +959,7 @@ async function resolveTargetChannelId(params) {
|
|
|
1313
959
|
});
|
|
1314
960
|
params.onDmChannelResolution?.(resolution);
|
|
1315
961
|
const channel = await resolution;
|
|
1316
|
-
dmChannelCache
|
|
962
|
+
cacheOutboundEntry(dmChannelCache, dmKey, channel.id, MATTERMOST_TARGET_CACHE_MAX_ENTRIES);
|
|
1317
963
|
return channel.id;
|
|
1318
964
|
}
|
|
1319
965
|
async function resolveMattermostSendContext(to, opts) {
|
|
@@ -1495,7 +1141,7 @@ function isDeletedMattermostCommand(command) {
|
|
|
1495
1141
|
return typeof command.delete_at === "number" && command.delete_at > 0;
|
|
1496
1142
|
}
|
|
1497
1143
|
function sanitizeCommandLookupError(error) {
|
|
1498
|
-
return (error instanceof Error ? error.message : String(error)).replace(/[\r\n\t]/gu, " ").replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => {
|
|
1144
|
+
return truncateUtf16Safe((error instanceof Error ? error.message : String(error)).replace(/[\r\n\t]/gu, " ").replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => {
|
|
1499
1145
|
try {
|
|
1500
1146
|
const url = new URL(urlText);
|
|
1501
1147
|
if (url.username || url.password) {
|
|
@@ -1507,10 +1153,10 @@ function sanitizeCommandLookupError(error) {
|
|
|
1507
1153
|
} catch {
|
|
1508
1154
|
return urlText;
|
|
1509
1155
|
}
|
|
1510
|
-
}).replace(/(^|[^\w-])(Bearer|Token)\s+[A-Za-z0-9._~+/=-]+/giu, "$1$2 [redacted]").replace(/\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu, "$1$2[redacted]")
|
|
1156
|
+
}).replace(/(^|[^\w-])(Bearer|Token)\s+[A-Za-z0-9._~+/=-]+/giu, "$1$2 [redacted]").replace(/\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu, "$1$2[redacted]"), 300);
|
|
1511
1157
|
}
|
|
1512
1158
|
function sanitizeMattermostLogValue(value) {
|
|
1513
|
-
return value.replace(/[\r\n\t]/gu, " ")
|
|
1159
|
+
return truncateUtf16Safe(value.replace(/[\r\n\t]/gu, " "), 200);
|
|
1514
1160
|
}
|
|
1515
1161
|
async function withCommandLookupTimeout(task) {
|
|
1516
1162
|
const controller = new AbortController();
|
|
@@ -2065,12 +1711,16 @@ function resolveSlashHandlerForToken(token) {
|
|
|
2065
1711
|
handler: state.handler
|
|
2066
1712
|
});
|
|
2067
1713
|
if (matches.length === 0) return { kind: "none" };
|
|
2068
|
-
if (matches.length === 1)
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
1714
|
+
if (matches.length === 1) {
|
|
1715
|
+
const match = matches[0];
|
|
1716
|
+
if (!match) return { kind: "none" };
|
|
1717
|
+
return {
|
|
1718
|
+
kind: "single",
|
|
1719
|
+
source: "token",
|
|
1720
|
+
handler: match.handler,
|
|
1721
|
+
accountIds: [match.accountId]
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
2074
1724
|
return {
|
|
2075
1725
|
kind: "ambiguous",
|
|
2076
1726
|
source: "token",
|
|
@@ -2086,12 +1736,16 @@ function resolveSlashHandlerForCommand(params) {
|
|
|
2086
1736
|
handler: state.handler
|
|
2087
1737
|
});
|
|
2088
1738
|
if (matches.length === 0) return { kind: "none" };
|
|
2089
|
-
if (matches.length === 1)
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
1739
|
+
if (matches.length === 1) {
|
|
1740
|
+
const match = matches[0];
|
|
1741
|
+
if (!match) return { kind: "none" };
|
|
1742
|
+
return {
|
|
1743
|
+
kind: "single",
|
|
1744
|
+
source: "command",
|
|
1745
|
+
handler: match.handler,
|
|
1746
|
+
accountIds: [match.accountId]
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
2095
1749
|
return {
|
|
2096
1750
|
kind: "ambiguous",
|
|
2097
1751
|
source: "command",
|
|
@@ -2190,8 +1844,8 @@ function registerSlashCommandRoute(api) {
|
|
|
2190
1844
|
return;
|
|
2191
1845
|
}
|
|
2192
1846
|
if (accountStates.size === 1) {
|
|
2193
|
-
const
|
|
2194
|
-
if (!state
|
|
1847
|
+
const state = accountStates.values().next().value;
|
|
1848
|
+
if (!state?.handler) {
|
|
2195
1849
|
res.statusCode = 503;
|
|
2196
1850
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2197
1851
|
res.end(JSON.stringify({
|
|
@@ -2270,4 +1924,4 @@ function registerSlashCommandRoute(api) {
|
|
|
2270
1924
|
});
|
|
2271
1925
|
}
|
|
2272
1926
|
//#endregion
|
|
2273
|
-
export { registerSlashCommands as
|
|
1927
|
+
export { registerSlashCommands as _, sendMessageMattermost as a, deliverMattermostReplyPayload as c, renderMattermostModelsPickerView as d, renderMattermostProviderPickerView as f, isSlashCommandsEnabled as g, cleanupSlashCommands as h, registerSlashCommandRoute as i, buildMattermostAllowedModelRefs as l, DEFAULT_COMMAND_SPECS as m, deactivateSlashCommands as n, resolveMattermostOpaqueTarget as o, resolveMattermostModelPickerCurrentModel as p, getSlashCommandState as r, createMattermostReplyDeliveryBarrier as s, activateSlashCommands as t, parseMattermostModelPickerContext as u, resolveCallbackUrl as v, resolveSlashCommandConfig as y };
|