@crewhaus/channel-adapter-slack 0.1.7 → 0.1.8

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/index.d.ts CHANGED
@@ -48,6 +48,24 @@ export type InboundEvent = {
48
48
  readonly text: string;
49
49
  readonly subtype: "app_mention" | "message";
50
50
  };
51
+ /**
52
+ * A 👍/👎 reaction a user placed on one of the bot's messages. Normalised from
53
+ * Slack's `reaction_added` event. `messageTs` is the reacted-to message's ts;
54
+ * `vote` is the emoji mapped to a thumbs direction. The session-router records
55
+ * this as a `user_feedback` event-log line rather than driving a model turn.
56
+ * Only meaningful emojis (👍/👎) reach here — the bot's own status reactions
57
+ * (👀/✅/⚠️) never map to a vote, so they never produce feedback.
58
+ */
59
+ export type InboundReaction = {
60
+ readonly idempotencyKey: string;
61
+ readonly workspaceId: string;
62
+ readonly channelId: string;
63
+ /** ts of the reacted-to message (typically the bot's reply). */
64
+ readonly messageTs: string;
65
+ /** the user who added the reaction. */
66
+ readonly userId: string;
67
+ readonly vote: "up" | "down";
68
+ };
51
69
  /**
52
70
  * The result of `parseInbound`. Three shapes:
53
71
  * - { kind: "event", event } — a real inbound message to route
@@ -59,6 +77,9 @@ export type InboundEvent = {
59
77
  export type ParsedInbound = {
60
78
  readonly kind: "event";
61
79
  readonly event: InboundEvent;
80
+ } | {
81
+ readonly kind: "reaction";
82
+ readonly reaction: InboundReaction;
62
83
  } | {
63
84
  readonly kind: "challenge";
64
85
  readonly challenge: string;
package/dist/index.js CHANGED
@@ -31,6 +31,19 @@ export class SlackAdapterError extends CrewhausError {
31
31
  super("channel", message, cause);
32
32
  }
33
33
  }
34
+ /** Map a Slack reaction name to a thumbs vote, or undefined when the emoji is
35
+ * not a 👍/👎 (which then produces no feedback). Slack delivers a skin-toned
36
+ * reaction as `+1::skin-tone-4`, so the base name is taken before matching. */
37
+ function reactionToVote(name) {
38
+ if (typeof name !== "string")
39
+ return undefined;
40
+ const base = name.replace(/^:|:$/g, "").split("::")[0] ?? "";
41
+ if (base === "+1" || base === "thumbsup")
42
+ return "up";
43
+ if (base === "-1" || base === "thumbsdown")
44
+ return "down";
45
+ return undefined;
46
+ }
34
47
  const DEFAULT_API_BASE_URL = "https://slack.com/api";
35
48
  export function createSlackAdapter(config, opts = {}) {
36
49
  const apiBaseUrl = opts.apiBaseUrl ?? process.env["SLACK_API_BASE_URL"] ?? DEFAULT_API_BASE_URL;
@@ -63,6 +76,40 @@ export function createSlackAdapter(config, opts = {}) {
63
76
  return { kind: "skip" };
64
77
  const e = ev;
65
78
  const evType = e["type"];
79
+ // Inbound 👍/👎 reaction on one of the bot's messages → feedback. Only
80
+ // reaction_added produces feedback (a removed reaction does not retract
81
+ // an append-only event-log line). Non-vote emojis (incl. the bot's own
82
+ // 👀/✅/⚠️ status reactions) map to no vote and are skipped.
83
+ if (evType === "reaction_added") {
84
+ const vote = reactionToVote(e["reaction"]);
85
+ if (vote === undefined)
86
+ return { kind: "skip" };
87
+ const item = e["item"];
88
+ if (typeof item !== "object" || item === null)
89
+ return { kind: "skip" };
90
+ const it = item;
91
+ if (it["type"] !== "message")
92
+ return { kind: "skip" };
93
+ const rChannelId = typeof it["channel"] === "string" ? it["channel"] : undefined;
94
+ const rMessageTs = typeof it["ts"] === "string" ? it["ts"] : undefined;
95
+ const rUserId = typeof e["user"] === "string" ? e["user"] : undefined;
96
+ const rIdemp = typeof p["event_id"] === "string" ? p["event_id"] : undefined;
97
+ const rWorkspace = typeof p["team_id"] === "string" ? p["team_id"] : undefined;
98
+ if (!rChannelId || !rMessageTs || !rUserId || !rIdemp || !rWorkspace) {
99
+ return { kind: "skip" };
100
+ }
101
+ return {
102
+ kind: "reaction",
103
+ reaction: {
104
+ idempotencyKey: rIdemp,
105
+ workspaceId: rWorkspace,
106
+ channelId: rChannelId,
107
+ messageTs: rMessageTs,
108
+ userId: rUserId,
109
+ vote,
110
+ },
111
+ };
112
+ }
66
113
  if (evType !== "app_mention" && evType !== "message")
67
114
  return { kind: "skip" };
68
115
  // Skip self/bot loops. `bot_id` is present on Slack's bot-authored
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/channel-adapter-slack",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Slack channel adapter: webhook signature verification, event parsing, reply-to-thread (Section 12)",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.1.7"
18
+ "@crewhaus/errors": "0.1.8"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {