@vellumai/assistant 0.8.3 → 0.8.4
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/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* implements the MessagingProvider interface.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
|
|
8
10
|
import {
|
|
9
11
|
buildSlackUserLabelMap,
|
|
10
12
|
renderSlackTextForModel,
|
|
@@ -35,10 +37,32 @@ import type {
|
|
|
35
37
|
SlackConversation,
|
|
36
38
|
SlackMessage,
|
|
37
39
|
SlackSearchMatch,
|
|
40
|
+
SlackUser,
|
|
38
41
|
} from "./types.js";
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
interface NormalizedSlackUserInfo {
|
|
44
|
+
displayName: string;
|
|
45
|
+
timezone?: string;
|
|
46
|
+
timezoneLabel?: string;
|
|
47
|
+
timezoneOffsetSeconds?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SlackUserInfoLookupResult {
|
|
51
|
+
info: NormalizedSlackUserInfo;
|
|
52
|
+
cacheable: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PERMANENT_USER_INFO_SLACK_ERRORS = new Set([
|
|
56
|
+
"account_inactive",
|
|
57
|
+
"ekm_access_denied",
|
|
58
|
+
"missing_scope",
|
|
59
|
+
"not_allowed_token_type",
|
|
60
|
+
"user_not_found",
|
|
61
|
+
"user_not_visible",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// Cache normalized Slack user facts to avoid repeated API calls within a session.
|
|
65
|
+
const userInfoCache = new Map<string, Promise<SlackUserInfoLookupResult>>();
|
|
42
66
|
|
|
43
67
|
/**
|
|
44
68
|
* Cached auth resolved during resolveConnection(), split by direction.
|
|
@@ -58,6 +82,7 @@ const userNameCache = new Map<string, string>();
|
|
|
58
82
|
*/
|
|
59
83
|
let _cachedSlackWriteAuth: OAuthConnection | string | null = null;
|
|
60
84
|
let _cachedSlackReadAuth: OAuthConnection | string | null = null;
|
|
85
|
+
const botUserIdByBotIdCache = new Map<string, string>();
|
|
61
86
|
|
|
62
87
|
/**
|
|
63
88
|
* Get the Slack auth value to pass to Slack client functions.
|
|
@@ -123,6 +148,32 @@ export async function withSlackBotToken<T>(
|
|
|
123
148
|
return auth.withToken(fn);
|
|
124
149
|
}
|
|
125
150
|
|
|
151
|
+
export async function resolveSlackBotUserId(
|
|
152
|
+
account: string | undefined,
|
|
153
|
+
botId: string,
|
|
154
|
+
): Promise<string | null> {
|
|
155
|
+
const trimmedBotId = botId.trim();
|
|
156
|
+
if (!trimmedBotId) return null;
|
|
157
|
+
|
|
158
|
+
const cacheKey = account ? `${account}:${trimmedBotId}` : null;
|
|
159
|
+
if (cacheKey && botUserIdByBotIdCache.has(cacheKey)) {
|
|
160
|
+
return botUserIdByBotIdCache.get(cacheKey) ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const resolvedUserId = await withSlackBotToken(account, async (token) => {
|
|
164
|
+
const resp = await slack.botsInfo(token, trimmedBotId);
|
|
165
|
+
const userId = resp.bot.user_id?.trim();
|
|
166
|
+
return userId && userId.length > 0 ? userId : null;
|
|
167
|
+
});
|
|
168
|
+
if (resolvedUserId) {
|
|
169
|
+
if (cacheKey) {
|
|
170
|
+
botUserIdByBotIdCache.set(cacheKey, resolvedUserId);
|
|
171
|
+
}
|
|
172
|
+
return resolvedUserId;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
126
177
|
/**
|
|
127
178
|
* Run a read-path Slack call, falling back to the bot token if the cached
|
|
128
179
|
* user token is rejected with an auth error. On fallback, the read cache is
|
|
@@ -159,20 +210,46 @@ async function resolveUserName(
|
|
|
159
210
|
auth: OAuthConnection | string,
|
|
160
211
|
userId: string,
|
|
161
212
|
): Promise<string> {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
213
|
+
return (await resolveUserInfo(auth, userId)).displayName;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function resolveUserInfo(
|
|
217
|
+
auth: OAuthConnection | string,
|
|
218
|
+
userId: string,
|
|
219
|
+
): Promise<NormalizedSlackUserInfo> {
|
|
220
|
+
if (!userId) return { displayName: "unknown" };
|
|
221
|
+
const cacheKey = slackUserInfoCacheKey(auth, userId);
|
|
222
|
+
const cached = userInfoCache.get(cacheKey);
|
|
223
|
+
if (cached) return (await cached).info;
|
|
224
|
+
|
|
225
|
+
const resolved = resolveUserInfoUncached(auth, userId).then(
|
|
226
|
+
(result) => {
|
|
227
|
+
if (!result.cacheable) {
|
|
228
|
+
userInfoCache.delete(cacheKey);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
(err) => {
|
|
233
|
+
userInfoCache.delete(cacheKey);
|
|
234
|
+
throw err;
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
userInfoCache.set(cacheKey, resolved);
|
|
238
|
+
return (await resolved).info;
|
|
239
|
+
}
|
|
165
240
|
|
|
166
|
-
|
|
241
|
+
async function resolveUserInfoUncached(
|
|
242
|
+
auth: OAuthConnection | string,
|
|
243
|
+
userId: string,
|
|
244
|
+
): Promise<SlackUserInfoLookupResult> {
|
|
245
|
+
let contactDisplayName: string | undefined;
|
|
167
246
|
try {
|
|
168
247
|
const result = findContactChannel({
|
|
169
248
|
channelType: "slack",
|
|
170
249
|
externalUserId: userId,
|
|
171
250
|
});
|
|
172
251
|
if (result) {
|
|
173
|
-
|
|
174
|
-
userNameCache.set(userId, name);
|
|
175
|
-
return name;
|
|
252
|
+
contactDisplayName = result.contact.displayName;
|
|
176
253
|
}
|
|
177
254
|
} catch {
|
|
178
255
|
// Contact lookup failures are non-fatal — fall through to API
|
|
@@ -180,18 +257,86 @@ async function resolveUserName(
|
|
|
180
257
|
|
|
181
258
|
try {
|
|
182
259
|
const resp = await slack.userInfo(auth, userId);
|
|
183
|
-
|
|
184
|
-
resp.user
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
260
|
+
return {
|
|
261
|
+
info: normalizeSlackUserInfo(resp.user, contactDisplayName),
|
|
262
|
+
cacheable: true,
|
|
263
|
+
};
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return {
|
|
266
|
+
info: { displayName: contactDisplayName ?? userId },
|
|
267
|
+
cacheable: isPermanentSlackUserInfoFailure(err),
|
|
268
|
+
};
|
|
192
269
|
}
|
|
193
270
|
}
|
|
194
271
|
|
|
272
|
+
function isPermanentSlackUserInfoFailure(err: unknown): boolean {
|
|
273
|
+
return (
|
|
274
|
+
err instanceof SlackApiError &&
|
|
275
|
+
PERMANENT_USER_INFO_SLACK_ERRORS.has(err.slackError)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function slackUserInfoCacheKey(
|
|
280
|
+
auth: OAuthConnection | string,
|
|
281
|
+
userId: string,
|
|
282
|
+
): string {
|
|
283
|
+
const authScope =
|
|
284
|
+
typeof auth === "string"
|
|
285
|
+
? `token:${createHash("sha256").update(auth).digest("hex")}`
|
|
286
|
+
: `connection:${auth.id}:${auth.accountInfo ?? ""}`;
|
|
287
|
+
return `${authScope}:user:${userId}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeSlackUserInfo(
|
|
291
|
+
user: SlackUser,
|
|
292
|
+
contactDisplayName: string | undefined,
|
|
293
|
+
): NormalizedSlackUserInfo {
|
|
294
|
+
const displayName =
|
|
295
|
+
contactDisplayName ||
|
|
296
|
+
user.profile?.display_name ||
|
|
297
|
+
user.profile?.real_name ||
|
|
298
|
+
user.real_name ||
|
|
299
|
+
user.name ||
|
|
300
|
+
user.id;
|
|
301
|
+
const timezone = trimNonEmpty(user.tz);
|
|
302
|
+
const timezoneLabel = trimNonEmpty(user.tz_label);
|
|
303
|
+
const timezoneOffsetSeconds =
|
|
304
|
+
typeof user.tz_offset === "number" && Number.isFinite(user.tz_offset)
|
|
305
|
+
? user.tz_offset
|
|
306
|
+
: undefined;
|
|
307
|
+
return {
|
|
308
|
+
displayName,
|
|
309
|
+
...(timezone ? { timezone } : {}),
|
|
310
|
+
...(timezoneLabel ? { timezoneLabel } : {}),
|
|
311
|
+
...(timezoneOffsetSeconds !== undefined ? { timezoneOffsetSeconds } : {}),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function trimNonEmpty(value: unknown): string | undefined {
|
|
316
|
+
if (typeof value !== "string") return undefined;
|
|
317
|
+
const trimmed = value.trim();
|
|
318
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function __resetSlackUserInfoCacheForTests(): void {
|
|
322
|
+
userInfoCache.clear();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function slackUserInfoMetadata(
|
|
326
|
+
userInfo: NormalizedSlackUserInfo | undefined,
|
|
327
|
+
): Record<string, unknown> {
|
|
328
|
+
if (!userInfo) return {};
|
|
329
|
+
return {
|
|
330
|
+
...(userInfo.timezone ? { actorTimezone: userInfo.timezone } : {}),
|
|
331
|
+
...(userInfo.timezoneLabel
|
|
332
|
+
? { actorTimezoneLabel: userInfo.timezoneLabel }
|
|
333
|
+
: {}),
|
|
334
|
+
...(userInfo.timezoneOffsetSeconds !== undefined
|
|
335
|
+
? { actorTimezoneOffsetSeconds: userInfo.timezoneOffsetSeconds }
|
|
336
|
+
: {}),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
195
340
|
function mapConversationType(conv: SlackConversation): Conversation["type"] {
|
|
196
341
|
if (conv.is_im) return "dm";
|
|
197
342
|
if (conv.is_mpim) return "group";
|
|
@@ -249,19 +394,25 @@ function mapSlackFiles(files: SlackMessage["files"]):
|
|
|
249
394
|
function mapMessage(
|
|
250
395
|
msg: SlackMessage,
|
|
251
396
|
channelId: string,
|
|
252
|
-
|
|
397
|
+
senderInfo: NormalizedSlackUserInfo,
|
|
253
398
|
renderedText: string,
|
|
254
399
|
): Message {
|
|
255
400
|
// Bot-authored when Slack sets `subtype: "bot_message"` or attributes the
|
|
256
|
-
// row to a `bot_id` with no user. Backfill callers
|
|
257
|
-
//
|
|
401
|
+
// row to a `bot_id` with no user. Backfill callers use this flag for
|
|
402
|
+
// bot-specific filtering while preserving real bot rows as channel replay.
|
|
258
403
|
const isBot =
|
|
259
404
|
msg.subtype === "bot_message" || (msg.bot_id != null && !msg.user);
|
|
260
405
|
const slackFiles = mapSlackFiles(msg.files);
|
|
406
|
+
const slackBotId = msg.bot_id?.trim();
|
|
407
|
+
const userMetadata = slackUserInfoMetadata(msg.user ? senderInfo : undefined);
|
|
408
|
+
const hasUserMetadata = Object.keys(userMetadata).length > 0;
|
|
261
409
|
return {
|
|
262
410
|
id: msg.ts,
|
|
263
411
|
conversationId: channelId,
|
|
264
|
-
sender: {
|
|
412
|
+
sender: {
|
|
413
|
+
id: msg.user ?? msg.bot_id ?? "unknown",
|
|
414
|
+
name: senderInfo.displayName,
|
|
415
|
+
},
|
|
265
416
|
text: renderedText,
|
|
266
417
|
timestamp: parseFloat(msg.ts) * 1000,
|
|
267
418
|
threadId: msg.thread_ts,
|
|
@@ -269,11 +420,13 @@ function mapMessage(
|
|
|
269
420
|
platform: "slack",
|
|
270
421
|
reactions: msg.reactions?.map((r) => ({ name: r.name, count: r.count })),
|
|
271
422
|
hasAttachments: (msg.files?.length ?? 0) > 0,
|
|
272
|
-
...(isBot || slackFiles
|
|
423
|
+
...(isBot || slackFiles || hasUserMetadata
|
|
273
424
|
? {
|
|
274
425
|
metadata: {
|
|
275
426
|
...(isBot ? { isBot: true } : {}),
|
|
427
|
+
...(slackBotId ? { slackBotId } : {}),
|
|
276
428
|
...(slackFiles ? { slackFiles } : {}),
|
|
429
|
+
...userMetadata,
|
|
277
430
|
},
|
|
278
431
|
}
|
|
279
432
|
: {}),
|
|
@@ -307,12 +460,12 @@ async function mapSlackMessages(
|
|
|
307
460
|
);
|
|
308
461
|
const messages: Message[] = [];
|
|
309
462
|
for (const msg of slackMessages) {
|
|
310
|
-
const
|
|
463
|
+
const senderInfo = await resolveUserInfo(auth, msg.user ?? "");
|
|
311
464
|
messages.push(
|
|
312
465
|
mapMessage(
|
|
313
466
|
msg,
|
|
314
467
|
channelId,
|
|
315
|
-
|
|
468
|
+
senderInfo,
|
|
316
469
|
renderSlackTextForModel(msg.text, { userLabels }),
|
|
317
470
|
),
|
|
318
471
|
);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterAll, afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const BOT_TOKEN = "xoxb-test";
|
|
4
|
+
|
|
5
|
+
mock.module("../../../security/secure-keys.js", () => ({
|
|
6
|
+
getSecureKeyAsync: async () => BOT_TOKEN,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const { getSlackConversationInfo } = await import("./api.js");
|
|
10
|
+
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
globalThis.fetch = originalFetch;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
mock.restore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("getSlackConversationInfo", () => {
|
|
22
|
+
test("calls conversations.info with GET query params", async () => {
|
|
23
|
+
let capturedUrl: string | undefined;
|
|
24
|
+
let capturedInit: RequestInit | undefined;
|
|
25
|
+
|
|
26
|
+
globalThis.fetch = mock(async (input, init) => {
|
|
27
|
+
capturedUrl = String(input);
|
|
28
|
+
capturedInit = init;
|
|
29
|
+
return new Response(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
ok: true,
|
|
32
|
+
channel: {
|
|
33
|
+
id: "C123",
|
|
34
|
+
name: "engineering",
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
38
|
+
);
|
|
39
|
+
}) as unknown as typeof fetch;
|
|
40
|
+
|
|
41
|
+
const info = await getSlackConversationInfo("C123");
|
|
42
|
+
|
|
43
|
+
expect(info).toEqual({ id: "C123", name: "engineering" });
|
|
44
|
+
expect(capturedUrl).toBeDefined();
|
|
45
|
+
const url = new URL(capturedUrl!);
|
|
46
|
+
expect(url.pathname).toBe("/api/conversations.info");
|
|
47
|
+
expect(url.searchParams.get("channel")).toBe("C123");
|
|
48
|
+
expect(capturedInit?.method).toBe("GET");
|
|
49
|
+
expect(capturedInit?.body).toBeUndefined();
|
|
50
|
+
expect(capturedInit?.headers).toEqual({
|
|
51
|
+
Authorization: `Bearer ${BOT_TOKEN}`,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -53,9 +53,7 @@ const SLACK_ERROR_CODE_MAP: Record<string, SlackErrorCategory> = {
|
|
|
53
53
|
invalid_blocks: "client_error",
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
function classifySlackError(
|
|
57
|
-
errorCode: string | undefined,
|
|
58
|
-
): SlackErrorCategory {
|
|
56
|
+
function classifySlackError(errorCode: string | undefined): SlackErrorCategory {
|
|
59
57
|
if (!errorCode) return "unknown";
|
|
60
58
|
return SLACK_ERROR_CODE_MAP[errorCode] ?? "unknown";
|
|
61
59
|
}
|
|
@@ -98,6 +96,20 @@ interface SlackApiResponse {
|
|
|
98
96
|
file_id?: string;
|
|
99
97
|
}
|
|
100
98
|
|
|
99
|
+
interface SlackConversationsInfoResponse extends SlackApiResponse {
|
|
100
|
+
channel?: {
|
|
101
|
+
id?: unknown;
|
|
102
|
+
name?: unknown;
|
|
103
|
+
name_normalized?: unknown;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SlackConversationInfo {
|
|
108
|
+
id: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
nameNormalized?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
/**
|
|
102
114
|
* Call a Slack Web API method with rate-limit retries.
|
|
103
115
|
*
|
|
@@ -178,6 +190,110 @@ export async function callSlackApi(
|
|
|
178
190
|
);
|
|
179
191
|
}
|
|
180
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Call a Slack Web API read method with query parameters.
|
|
195
|
+
*/
|
|
196
|
+
async function callSlackApiGet(
|
|
197
|
+
method: string,
|
|
198
|
+
params: URLSearchParams,
|
|
199
|
+
): Promise<SlackApiResponse> {
|
|
200
|
+
const botToken = await resolveBotToken();
|
|
201
|
+
const query = params.toString();
|
|
202
|
+
const url = `${SLACK_API_BASE}/${method}${query ? `?${query}` : ""}`;
|
|
203
|
+
|
|
204
|
+
let lastError: string | undefined;
|
|
205
|
+
|
|
206
|
+
for (let attempt = 0; attempt <= SLACK_MAX_RATE_LIMIT_RETRIES; attempt++) {
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
method: "GET",
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: `Bearer ${botToken}`,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (response.status === 429) {
|
|
215
|
+
if (attempt >= SLACK_MAX_RATE_LIMIT_RETRIES) {
|
|
216
|
+
throw new Error("Slack rate limit exceeded after retries");
|
|
217
|
+
}
|
|
218
|
+
const retryAfter =
|
|
219
|
+
parseInt(response.headers.get("Retry-After") ?? "", 10) ||
|
|
220
|
+
SLACK_DEFAULT_RETRY_AFTER_S;
|
|
221
|
+
log.warn({ method, retryAfter, attempt }, "Slack rate limited, retrying");
|
|
222
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1000));
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (response.status >= 500) {
|
|
227
|
+
if (attempt >= SLACK_MAX_RATE_LIMIT_RETRIES) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Slack ${method} failed with status ${response.status} after retries`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
log.warn(
|
|
233
|
+
{ method, status: response.status, attempt },
|
|
234
|
+
"Slack 5xx error, retrying",
|
|
235
|
+
);
|
|
236
|
+
await new Promise((r) =>
|
|
237
|
+
setTimeout(r, SLACK_DEFAULT_RETRY_AFTER_S * 1000),
|
|
238
|
+
);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const data = (await response.json()) as SlackApiResponse;
|
|
243
|
+
|
|
244
|
+
if (!data.ok) {
|
|
245
|
+
lastError = data.error;
|
|
246
|
+
const category = classifySlackError(data.error);
|
|
247
|
+
|
|
248
|
+
if (category === "rate_limit" && attempt < SLACK_MAX_RATE_LIMIT_RETRIES) {
|
|
249
|
+
log.warn(
|
|
250
|
+
{ method, slackError: data.error, attempt },
|
|
251
|
+
"Slack rate limited (body), retrying",
|
|
252
|
+
);
|
|
253
|
+
await new Promise((r) =>
|
|
254
|
+
setTimeout(r, SLACK_DEFAULT_RETRY_AFTER_S * 1000),
|
|
255
|
+
);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw new SlackApiError(data.error);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return data;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Slack ${method} failed after retries: ${lastError ?? "unknown"}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizeSlackString(value: unknown): string | undefined {
|
|
271
|
+
if (typeof value !== "string") return undefined;
|
|
272
|
+
const trimmed = value.trim();
|
|
273
|
+
return trimmed || undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function getSlackConversationInfo(
|
|
277
|
+
channelId: string,
|
|
278
|
+
): Promise<SlackConversationInfo | null> {
|
|
279
|
+
const data = (await callSlackApiGet(
|
|
280
|
+
"conversations.info",
|
|
281
|
+
new URLSearchParams({ channel: channelId }),
|
|
282
|
+
)) as SlackConversationsInfoResponse;
|
|
283
|
+
|
|
284
|
+
const id = normalizeSlackString(data.channel?.id);
|
|
285
|
+
if (!id) return null;
|
|
286
|
+
|
|
287
|
+
const name = normalizeSlackString(data.channel?.name);
|
|
288
|
+
const nameNormalized = normalizeSlackString(data.channel?.name_normalized);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
id,
|
|
292
|
+
...(name ? { name } : {}),
|
|
293
|
+
...(nameNormalized ? { nameNormalized } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
181
297
|
/**
|
|
182
298
|
* Call a Slack Web API method with form-urlencoded body.
|
|
183
299
|
*/
|
|
@@ -14,6 +14,7 @@ import type { OAuthConnection } from "../../../oauth/connection.js";
|
|
|
14
14
|
import type {
|
|
15
15
|
SlackApiResponse,
|
|
16
16
|
SlackAuthTestResponse,
|
|
17
|
+
SlackBotsInfoResponse,
|
|
17
18
|
SlackConversationHistoryResponse,
|
|
18
19
|
SlackConversationMarkResponse,
|
|
19
20
|
SlackConversationRepliesResponse,
|
|
@@ -286,6 +287,17 @@ export async function authTest(
|
|
|
286
287
|
return request<SlackAuthTestResponse>(connectionOrToken, "auth.test");
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
export async function botsInfo(
|
|
291
|
+
connectionOrToken: OAuthConnection | string,
|
|
292
|
+
botId: string,
|
|
293
|
+
teamId?: string,
|
|
294
|
+
): Promise<SlackBotsInfoResponse> {
|
|
295
|
+
return request<SlackBotsInfoResponse>(connectionOrToken, "bots.info", {
|
|
296
|
+
bot: botId,
|
|
297
|
+
team_id: teamId,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
289
301
|
export async function listConversations(
|
|
290
302
|
connectionOrToken: OAuthConnection | string,
|
|
291
303
|
types = "public_channel,private_channel,mpim,im",
|
|
@@ -40,13 +40,31 @@ export function buildSlackWebMessageUrl(params: {
|
|
|
40
40
|
teamUrl?: string | null;
|
|
41
41
|
channelId: string;
|
|
42
42
|
messageTs: string;
|
|
43
|
+
threadTs?: string;
|
|
43
44
|
}): string | undefined {
|
|
44
45
|
const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
|
|
45
46
|
if (!teamUrl) return undefined;
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
const baseUrl = `${teamUrl}/archives/${encodeURIComponent(
|
|
48
49
|
params.channelId,
|
|
49
50
|
)}/p${formatSlackPermalinkTimestamp(params.messageTs)}`;
|
|
51
|
+
if (!params.threadTs) return baseUrl;
|
|
52
|
+
|
|
53
|
+
const search = new URLSearchParams({
|
|
54
|
+
thread_ts: params.threadTs,
|
|
55
|
+
cid: params.channelId,
|
|
56
|
+
});
|
|
57
|
+
return `${baseUrl}?${search.toString()}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildSlackWebChannelUrl(params: {
|
|
61
|
+
teamUrl?: string | null;
|
|
62
|
+
channelId: string;
|
|
63
|
+
}): string | undefined {
|
|
64
|
+
const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
|
|
65
|
+
if (!teamUrl) return undefined;
|
|
66
|
+
|
|
67
|
+
return `${teamUrl}/archives/${encodeURIComponent(params.channelId)}`;
|
|
50
68
|
}
|
|
51
69
|
|
|
52
70
|
export function buildSlackMessageDeepLinks(params: {
|
|
@@ -54,6 +72,7 @@ export function buildSlackMessageDeepLinks(params: {
|
|
|
54
72
|
teamUrl?: string | null;
|
|
55
73
|
channelId: string;
|
|
56
74
|
messageTs: string;
|
|
75
|
+
threadTs?: string;
|
|
57
76
|
}): SlackMessageDeepLinks | undefined {
|
|
58
77
|
const appUrl = buildSlackAppMessageUrl(params);
|
|
59
78
|
const webUrl = buildSlackWebMessageUrl(params);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
buildSlackTimezoneMetadata,
|
|
5
|
+
formatSlackTimezoneLabel,
|
|
4
6
|
mergeSlackMetadata,
|
|
5
7
|
readSlackMetadata,
|
|
6
8
|
readSlackMetadataFromMessageMetadata,
|
|
@@ -8,6 +10,42 @@ import {
|
|
|
8
10
|
writeSlackMetadata,
|
|
9
11
|
} from "./message-metadata.js";
|
|
10
12
|
|
|
13
|
+
describe("formatSlackTimezoneLabel", () => {
|
|
14
|
+
test("uses compact common labels for IANA timezones", () => {
|
|
15
|
+
expect(formatSlackTimezoneLabel("America/Denver")).toBe("MT");
|
|
16
|
+
expect(formatSlackTimezoneLabel("America/New_York")).toBe("ET");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("compacts persisted Slack profile labels before falling back to timezone", () => {
|
|
20
|
+
expect(
|
|
21
|
+
formatSlackTimezoneLabel("America/New_York", {
|
|
22
|
+
persistedLabel: "Eastern Time",
|
|
23
|
+
}),
|
|
24
|
+
).toBe("ET");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("buildSlackTimezoneMetadata", () => {
|
|
29
|
+
test("copies only populated Slack timezone fields", () => {
|
|
30
|
+
expect(
|
|
31
|
+
buildSlackTimezoneMetadata({
|
|
32
|
+
actorTimezone: " America/New_York ",
|
|
33
|
+
actorTimezoneLabel: " ET ",
|
|
34
|
+
actorTimezoneOffsetSeconds: -18_000,
|
|
35
|
+
timestampTimezone: "America/New_York",
|
|
36
|
+
timestampTimezoneLabel: "",
|
|
37
|
+
speakerTimezoneLabel: " Eastern Time ",
|
|
38
|
+
}),
|
|
39
|
+
).toEqual({
|
|
40
|
+
actorTimezone: "America/New_York",
|
|
41
|
+
actorTimezoneLabel: "ET",
|
|
42
|
+
actorTimezoneOffsetSeconds: -18_000,
|
|
43
|
+
timestampTimezone: "America/New_York",
|
|
44
|
+
speakerTimezoneLabel: "Eastern Time",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
11
49
|
describe("readSlackMetadata", () => {
|
|
12
50
|
test("tolerates null and undefined", () => {
|
|
13
51
|
expect(readSlackMetadata(null)).toBeNull();
|
|
@@ -62,9 +100,17 @@ describe("readSlackMetadata", () => {
|
|
|
62
100
|
eventKind: "message",
|
|
63
101
|
editedAt: "not-a-number",
|
|
64
102
|
});
|
|
103
|
+
const badChannelName = JSON.stringify({
|
|
104
|
+
source: "slack",
|
|
105
|
+
channelId: "C123",
|
|
106
|
+
channelName: 42,
|
|
107
|
+
channelTs: "1700000000.000100",
|
|
108
|
+
eventKind: "message",
|
|
109
|
+
});
|
|
65
110
|
expect(readSlackMetadata(badThreadTs)).toBeNull();
|
|
66
111
|
expect(readSlackMetadata(badReactionOp)).toBeNull();
|
|
67
112
|
expect(readSlackMetadata(badEditedAt)).toBeNull();
|
|
113
|
+
expect(readSlackMetadata(badChannelName)).toBeNull();
|
|
68
114
|
});
|
|
69
115
|
|
|
70
116
|
test("strips unknown top-level keys from the returned object", () => {
|
|
@@ -111,6 +157,7 @@ describe("readSlackMetadata", () => {
|
|
|
111
157
|
const meta: SlackMessageMetadata = {
|
|
112
158
|
source: "slack",
|
|
113
159
|
channelId: "C123",
|
|
160
|
+
channelName: "engineering",
|
|
114
161
|
channelTs: "1700000000.000100",
|
|
115
162
|
threadTs: "1699999999.000000",
|
|
116
163
|
displayName: "Alice",
|
|
@@ -174,6 +221,7 @@ describe("writeSlackMetadata", () => {
|
|
|
174
221
|
const meta: SlackMessageMetadata = {
|
|
175
222
|
source: "slack",
|
|
176
223
|
channelId: "C123",
|
|
224
|
+
channelName: "engineering",
|
|
177
225
|
channelTs: "1700000000.000100",
|
|
178
226
|
threadTs: "1699999999.000000",
|
|
179
227
|
displayName: "Alice",
|