mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
feat(slack): publish App Home tab views
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
|
||||
- Discord: keep active buttons, selects, and forms working across Gateway restarts until they expire, so multi-step Discord interactions are less likely to break during upgrades or restarts. Thanks @amknight.
|
||||
- Messages/docs: clarify that `BodyForAgent` is the primary inbound model text while `Body` is the legacy envelope fallback, and add Signal coverage so channel hardening patches target the real prompt path. Refs #66198. Thanks @defonota3box.
|
||||
- Slack: publish a safe default App Home tab view on `app_home_opened` and include the Home tab event in setup manifests. Fixes #11655; refs #52020. Thanks @TinyTb.
|
||||
- Slack: keep track of bot-participated threads across restarts, so ongoing threaded conversations can continue auto-replying after the Gateway is restarted. Thanks @amknight.
|
||||
- Control UI/Usage: add UTC quarter-hour token buckets for the Usage Mosaic and reuse them for hour filtering, keeping the legacy session-span fallback for older summaries. (#74337) Thanks @konanok.
|
||||
- BlueBubbles: add opt-in `channels.bluebubbles.replyContextApiFallback` that fetches the original message from the BlueBubbles HTTP API when the in-memory reply-context cache misses (multi-instance deployments sharing one BB account, post-restart, after long-lived TTL/LRU eviction). Off by default; channel-level setting propagates to accounts that omit the flag through `mergeAccountConfig`; routed through the typed `BlueBubblesClient` so every fetch is SSRF-guarded by the same three-mode policy as every other BB client request; reply-id shape is validated and part-index prefixes (`p:0/<guid>`) are stripped before the request; concurrent webhooks for the same `replyToId` coalesce into one fetch and successful responses populate the reply cache for subsequent hits. Also promotes BlueBubbles attachment download failures from verbose to runtime error so silently-dropped inbound images are visible at default log level, and extends `sanitizeForLog` to redact `?password=…`/`?token=…` query params and `Authorization:` headers before they reach the log sink (CWE-532). (#71820) Thanks @coletebou and @zqchris.
|
||||
|
||||
@@ -169,6 +169,7 @@ Base manifest (Socket Mode default):
|
||||
"features": {
|
||||
"bot_user": { "display_name": "OpenClaw", "always_online": true },
|
||||
"app_home": {
|
||||
"home_tab_enabled": true,
|
||||
"messages_tab_enabled": true,
|
||||
"messages_tab_read_only_enabled": false
|
||||
},
|
||||
@@ -212,6 +213,7 @@ Base manifest (Socket Mode default):
|
||||
"socket_mode_enabled": true,
|
||||
"event_subscriptions": {
|
||||
"bot_events": [
|
||||
"app_home_opened",
|
||||
"app_mention",
|
||||
"channel_rename",
|
||||
"member_joined_channel",
|
||||
@@ -264,6 +266,8 @@ For **HTTP Request URLs mode**, replace `settings` with the HTTP variant and add
|
||||
|
||||
Surface different features that extend the above defaults.
|
||||
|
||||
The default manifest enables the Slack App Home **Home** tab and subscribes to `app_home_opened`. When a workspace member opens the Home tab, OpenClaw publishes a safe default Home view with `views.publish`; no conversation payload or private configuration is included. The **Messages** tab remains enabled for Slack DMs.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Optional native slash commands">
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ResolvedSlackAccount } from "../accounts.js";
|
||||
import type { SlackMonitorContext } from "./context.js";
|
||||
import { registerSlackChannelEvents } from "./events/channels.js";
|
||||
import { registerSlackHomeEvents } from "./events/home.js";
|
||||
import { registerSlackInteractionEvents } from "./events/interactions.js";
|
||||
import { registerSlackMemberEvents } from "./events/members.js";
|
||||
import { registerSlackMessageEvents } from "./events/messages.js";
|
||||
@@ -23,5 +24,6 @@ export function registerSlackMonitorEvents(params: {
|
||||
registerSlackMemberEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackChannelEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackPinEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackHomeEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackInteractionEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
}
|
||||
|
||||
102
extensions/slack/src/monitor/events/home.test.ts
Normal file
102
extensions/slack/src/monitor/events/home.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let buildSlackHomeView: typeof import("./home.js").buildSlackHomeView;
|
||||
let registerSlackHomeEvents: typeof import("./home.js").registerSlackHomeEvents;
|
||||
let createSlackSystemEventTestHarness: typeof import("./system-event-test-harness.js").createSlackSystemEventTestHarness;
|
||||
|
||||
type HomeHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
|
||||
|
||||
function createHomeContext(params?: {
|
||||
trackEvent?: () => void;
|
||||
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
|
||||
}) {
|
||||
const harness = createSlackSystemEventTestHarness();
|
||||
const publish = vi.fn().mockResolvedValue({ ok: true });
|
||||
if (params?.shouldDropMismatchedSlackEvent) {
|
||||
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
|
||||
}
|
||||
harness.ctx.botToken = "xoxb-test";
|
||||
(harness.ctx.app as unknown as { client: { views: { publish: typeof publish } } }).client = {
|
||||
views: { publish },
|
||||
};
|
||||
registerSlackHomeEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
|
||||
return {
|
||||
publish,
|
||||
getHomeHandler: () => harness.getHandler("app_home_opened") as HomeHandler | null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("registerSlackHomeEvents", () => {
|
||||
beforeAll(async () => {
|
||||
({ buildSlackHomeView, registerSlackHomeEvents } = await import("./home.js"));
|
||||
({ createSlackSystemEventTestHarness } = await import("./system-event-test-harness.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("publishes the default Home tab view for app_home_opened", async () => {
|
||||
const trackEvent = vi.fn();
|
||||
const { publish, getHomeHandler } = createHomeContext({ trackEvent });
|
||||
const handler = getHomeHandler();
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
await handler!({
|
||||
event: {
|
||||
type: "app_home_opened",
|
||||
user: "U123",
|
||||
channel: "D123",
|
||||
tab: "home",
|
||||
event_ts: "123.456",
|
||||
},
|
||||
body: { api_app_id: "A1" },
|
||||
});
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(publish).toHaveBeenCalledTimes(1);
|
||||
expect(publish).toHaveBeenCalledWith({
|
||||
token: "xoxb-test",
|
||||
user_id: "U123",
|
||||
view: buildSlackHomeView(),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not publish when Slack reports the Messages tab", async () => {
|
||||
const trackEvent = vi.fn();
|
||||
const { publish, getHomeHandler } = createHomeContext({ trackEvent });
|
||||
|
||||
await getHomeHandler()!({
|
||||
event: {
|
||||
type: "app_home_opened",
|
||||
user: "U123",
|
||||
channel: "D123",
|
||||
tab: "messages",
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledTimes(1);
|
||||
expect(publish).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not track or publish mismatched events", async () => {
|
||||
const trackEvent = vi.fn();
|
||||
const { publish, getHomeHandler } = createHomeContext({
|
||||
trackEvent,
|
||||
shouldDropMismatchedSlackEvent: () => true,
|
||||
});
|
||||
|
||||
await getHomeHandler()!({
|
||||
event: {
|
||||
type: "app_home_opened",
|
||||
user: "U123",
|
||||
tab: "home",
|
||||
},
|
||||
body: { api_app_id: "A_OTHER" },
|
||||
});
|
||||
|
||||
expect(trackEvent).not.toHaveBeenCalled();
|
||||
expect(publish).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
70
extensions/slack/src/monitor/events/home.ts
Normal file
70
extensions/slack/src/monitor/events/home.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import type { HomeView } from "@slack/types";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackAppHomeOpenedEvent } from "../types.js";
|
||||
|
||||
export function buildSlackHomeView(): HomeView {
|
||||
return {
|
||||
type: "home",
|
||||
callback_id: "openclaw:home",
|
||||
blocks: [
|
||||
{
|
||||
type: "header",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "OpenClaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "Send a DM, mention OpenClaw in a channel, or use `/openclaw` to start a session.",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "This Home tab is safe to show to any workspace member who opens the app.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function registerSlackHomeEvents(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
trackEvent?: () => void;
|
||||
}) {
|
||||
const { ctx, trackEvent } = params;
|
||||
|
||||
ctx.app.event(
|
||||
"app_home_opened",
|
||||
async ({ event, body }: SlackEventMiddlewareArgs<"app_home_opened">) => {
|
||||
try {
|
||||
if (ctx.shouldDropMismatchedSlackEvent(body)) {
|
||||
return;
|
||||
}
|
||||
trackEvent?.();
|
||||
|
||||
const payload = event as SlackAppHomeOpenedEvent;
|
||||
if (!payload.user || payload.tab === "messages") {
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.app.client.views.publish({
|
||||
token: ctx.botToken,
|
||||
user_id: payload.user,
|
||||
view: buildSlackHomeView(),
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -60,6 +60,14 @@ export type SlackChannelIdChangedEvent = {
|
||||
event_ts?: string;
|
||||
};
|
||||
|
||||
export type SlackAppHomeOpenedEvent = {
|
||||
type: "app_home_opened";
|
||||
user?: string;
|
||||
channel?: string;
|
||||
tab?: "home" | "messages";
|
||||
event_ts?: string;
|
||||
};
|
||||
|
||||
export type SlackPinEvent = {
|
||||
type: "pin_added" | "pin_removed";
|
||||
channel_id?: string;
|
||||
|
||||
@@ -20,6 +20,7 @@ function buildSlackManifest(botName: string) {
|
||||
always_online: true,
|
||||
},
|
||||
app_home: {
|
||||
home_tab_enabled: true,
|
||||
messages_tab_enabled: true,
|
||||
messages_tab_read_only_enabled: false,
|
||||
},
|
||||
@@ -63,6 +64,7 @@ function buildSlackManifest(botName: string) {
|
||||
socket_mode_enabled: true,
|
||||
event_subscriptions: {
|
||||
bot_events: [
|
||||
"app_home_opened",
|
||||
"app_mention",
|
||||
"channel_rename",
|
||||
"member_joined_channel",
|
||||
@@ -87,8 +89,8 @@ export function buildSlackSetupLines(botName = "OpenClaw"): string[] {
|
||||
"1) Slack API -> Create App -> From scratch or From manifest (with the JSON below)",
|
||||
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
|
||||
"3) Install App to workspace to get the xoxb- bot token",
|
||||
"4) Enable Event Subscriptions (socket) for message events",
|
||||
"5) App Home -> enable the Messages tab for DMs",
|
||||
"4) Enable Event Subscriptions (socket) for message and App Home events",
|
||||
"5) App Home -> enable the Home tab and Messages tab for DMs",
|
||||
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
|
||||
`Docs: ${formatDocsLink("/slack", "slack")}`,
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user