opencode-discord-bot 0.0.11 → 0.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord-bot",
3
- "version": "0.0.11",
3
+ "version": "0.1.0",
4
4
  "description": "Discord bot bridge for a self-hosted opencode instance",
5
5
  "type": "module",
6
6
  "main": "src/Main.ts",
@@ -3,7 +3,7 @@ import { basename } from "node:path"
3
3
  import { createDiscordAdapter, type DiscordThreadId } from "@chat-adapter/discord"
4
4
  import type { AdapterPostableMessage, FetchOptions, FetchResult, Message, PostableRaw, RawMessage } from "chat"
5
5
  import { Duration, Effect } from "effect"
6
- import type { DiscordAttachment, DiscordMessage, DiscordScope } from "../Schema.ts"
6
+ import type { DiscordAttachment, DiscordMessage, DiscordReaction, DiscordScope } from "../Schema.ts"
7
7
  import { DiscordError, type DiscordService } from "./DiscordPort.ts"
8
8
 
9
9
  type ChatDiscordAdapter = {
@@ -52,6 +52,23 @@ const attachments = (message: Message<unknown>): ReadonlyArray<DiscordAttachment
52
52
  url: item.url ?? ""
53
53
  }))
54
54
 
55
+ const reactionEmoji = (value: unknown): string => {
56
+ if (!isRecord(value)) return "unknown"
57
+ const name = typeof value.name === "string" && value.name.length > 0 ? value.name : undefined
58
+ const id = typeof value.id === "string" && value.id.length > 0 ? value.id : undefined
59
+ if (id !== undefined && name !== undefined) return `${value.animated === true ? "a:" : ""}${name}:${id}`
60
+ return name ?? id ?? "unknown"
61
+ }
62
+
63
+ const reactions = (message: Message<unknown>): ReadonlyArray<DiscordReaction> => {
64
+ if (!isRecord(message.raw)) return []
65
+ const rawReactions: ReadonlyArray<unknown> = Array.isArray(message.raw.reactions) ? message.raw.reactions : []
66
+ return rawReactions.flatMap((item) => {
67
+ if (!isRecord(item) || typeof item.count !== "number" || !Number.isFinite(item.count) || item.count < 0) return []
68
+ return [{ emoji: reactionEmoji(item.emoji), count: item.count }]
69
+ })
70
+ }
71
+
55
72
  const fromChatMessage = (scope: DiscordScope, message: Message<unknown>, nickname?: string | undefined): DiscordMessage => ({
56
73
  id: message.id,
57
74
  guildId: scope.guildId,
@@ -70,7 +87,7 @@ const fromChatMessage = (scope: DiscordScope, message: Message<unknown>, nicknam
70
87
  everyoneMention: message.text.includes("@everyone"),
71
88
  hereMention: message.text.includes("@here"),
72
89
  attachments: attachments(message),
73
- reactions: [],
90
+ reactions: reactions(message),
74
91
  channelType: "guild"
75
92
  })
76
93
 
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import type { DiscordThreadId } from "@chat-adapter/discord"
3
+ import type { AdapterPostableMessage, FetchOptions, FetchResult, RawMessage } from "chat"
4
+ import { Message, parseMarkdown } from "chat"
5
+ import { Effect } from "effect"
6
+ import type { DiscordScope } from "../Schema.ts"
7
+ import { makeChatSdkDiscord } from "./ChatSdkDiscord.ts"
8
+
9
+ const scope: DiscordScope = { guildId: "g1", channelId: "c1" }
10
+
11
+ const makeMessage = (threadId: string, raw: unknown): Message<unknown> =>
12
+ new Message({
13
+ id: "m1",
14
+ threadId,
15
+ text: "hello",
16
+ formatted: parseMarkdown("hello"),
17
+ raw,
18
+ author: { userId: "u1", userName: "alice", fullName: "Alice", isBot: false, isMe: false },
19
+ metadata: { dateSent: new Date("2026-06-05T14:03:00.000Z"), edited: false },
20
+ attachments: []
21
+ })
22
+
23
+ class ReactionAdapter {
24
+ encodeThreadId(input: DiscordThreadId): string {
25
+ return `discord:${input.guildId}:${input.channelId}`
26
+ }
27
+
28
+ fetchMessages(threadId: string, _options?: FetchOptions): Promise<FetchResult<unknown>> {
29
+ return Promise.resolve({
30
+ messages: [
31
+ makeMessage(threadId, {
32
+ reactions: [
33
+ { count: 3, emoji: { id: null, name: "\u{1F680}" } },
34
+ { count: 2, emoji: { id: "custom1", name: "party_blob", animated: false } },
35
+ { count: 1, emoji: { id: "anim1", name: "dance", animated: true } },
36
+ { count: "bad", emoji: { id: null, name: "ignored" } },
37
+ { count: -1, emoji: { id: null, name: "ignored" } }
38
+ ]
39
+ })
40
+ ]
41
+ })
42
+ }
43
+
44
+ postMessage(threadId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>> {
45
+ return Promise.resolve({ id: "posted", raw: {}, threadId })
46
+ }
47
+
48
+ editMessage(threadId: string, messageId: string, _message: AdapterPostableMessage): Promise<RawMessage<unknown>> {
49
+ return Promise.resolve({ id: messageId, raw: {}, threadId })
50
+ }
51
+
52
+ deleteMessage(_threadId: string, _messageId: string): Promise<void> {
53
+ return Promise.resolve()
54
+ }
55
+
56
+ startTyping(_threadId: string, _status?: string): Promise<void> {
57
+ return Promise.resolve()
58
+ }
59
+
60
+ addReaction(_threadId: string, _messageId: string, _emoji: string): Promise<void> {
61
+ return Promise.resolve()
62
+ }
63
+ }
64
+
65
+ describe("makeChatSdkDiscord reactions", () => {
66
+ test("maps aggregate reactions from raw Discord messages", async () => {
67
+ const discord = makeChatSdkDiscord(new ReactionAdapter())
68
+
69
+ const context = await Effect.runPromise(discord.fetchContext(scope, 30))
70
+
71
+ expect(context[0]?.reactions).toEqual([
72
+ { emoji: "\u{1F680}", count: 3 },
73
+ { emoji: "party_blob:custom1", count: 2 },
74
+ { emoji: "a:dance:anim1", count: 1 }
75
+ ])
76
+ })
77
+ })