@jcheesepkg/nanobot 0.9.1 → 0.9.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/README.md +18 -18
- package/dist/agent/context.d.mts +4 -4
- package/dist/agent/context.d.mts.map +1 -1
- package/dist/agent/context.mjs +27 -28
- package/dist/agent/context.mjs.map +1 -1
- package/dist/agent/loop.d.mts +5 -3
- package/dist/agent/loop.d.mts.map +1 -1
- package/dist/agent/loop.mjs +64 -55
- package/dist/agent/loop.mjs.map +1 -1
- package/dist/agent/memory.d.mts.map +1 -1
- package/dist/agent/memory.mjs +3 -3
- package/dist/agent/memory.mjs.map +1 -1
- package/dist/agent/skills.d.mts.map +1 -1
- package/dist/agent/skills.mjs +4 -4
- package/dist/agent/skills.mjs.map +1 -1
- package/dist/agent/subagent.d.mts.map +1 -1
- package/dist/agent/subagent.mjs +22 -22
- package/dist/agent/subagent.mjs.map +1 -1
- package/dist/agent/tools/base.mjs +2 -2
- package/dist/agent/tools/base.mjs.map +1 -1
- package/dist/agent/tools/cron.d.mts +1 -1
- package/dist/agent/tools/cron.d.mts.map +1 -1
- package/dist/agent/tools/cron.mjs +11 -11
- package/dist/agent/tools/cron.mjs.map +1 -1
- package/dist/agent/tools/filesystem.d.mts +4 -4
- package/dist/agent/tools/filesystem.d.mts.map +1 -1
- package/dist/agent/tools/filesystem.mjs +20 -20
- package/dist/agent/tools/filesystem.mjs.map +1 -1
- package/dist/agent/tools/flex.d.mts +1 -1
- package/dist/agent/tools/flex.d.mts.map +1 -1
- package/dist/agent/tools/flex.mjs +112 -112
- package/dist/agent/tools/flex.mjs.map +1 -1
- package/dist/agent/tools/flex.test.mjs +60 -59
- package/dist/agent/tools/flex.test.mjs.map +1 -1
- package/dist/agent/tools/message.d.mts +1 -1
- package/dist/agent/tools/message.d.mts.map +1 -1
- package/dist/agent/tools/message.mjs +4 -4
- package/dist/agent/tools/message.mjs.map +1 -1
- package/dist/agent/tools/registry.d.mts.map +1 -1
- package/dist/agent/tools/registry.mjs +4 -4
- package/dist/agent/tools/registry.mjs.map +1 -1
- package/dist/agent/tools/shell.d.mts +1 -1
- package/dist/agent/tools/shell.mjs +4 -4
- package/dist/agent/tools/shell.mjs.map +1 -1
- package/dist/agent/tools/spawn.d.mts +1 -1
- package/dist/agent/tools/spawn.d.mts.map +1 -1
- package/dist/agent/tools/spawn.mjs +4 -4
- package/dist/agent/tools/spawn.mjs.map +1 -1
- package/dist/agent/tools/web.d.mts +2 -2
- package/dist/agent/tools/web.d.mts.map +1 -1
- package/dist/agent/tools/web.mjs +36 -36
- package/dist/agent/tools/web.mjs.map +1 -1
- package/dist/bus/events.mjs +1 -1
- package/dist/bus/events.mjs.map +1 -1
- package/dist/bus/queue.d.mts.map +1 -1
- package/dist/bus/queue.mjs.map +1 -1
- package/dist/channels/base.d.mts.map +1 -1
- package/dist/channels/base.mjs +2 -2
- package/dist/channels/base.mjs.map +1 -1
- package/dist/channels/line.d.mts +1 -0
- package/dist/channels/line.d.mts.map +1 -1
- package/dist/channels/line.mjs +65 -65
- package/dist/channels/line.mjs.map +1 -1
- package/dist/channels/line.test.mjs +26 -27
- package/dist/channels/line.test.mjs.map +1 -1
- package/dist/channels/manager.d.mts.map +1 -1
- package/dist/channels/manager.mjs +9 -9
- package/dist/channels/manager.mjs.map +1 -1
- package/dist/channels/telegram.mjs +34 -34
- package/dist/channels/telegram.mjs.map +1 -1
- package/dist/cli/index.mjs +36 -36
- package/dist/cli/index.mjs.map +1 -1
- package/dist/config/loader.d.mts.map +1 -1
- package/dist/config/loader.mjs +1 -1
- package/dist/config/loader.mjs.map +1 -1
- package/dist/config/schema.d.mts +387 -387
- package/dist/config/schema.d.mts.map +1 -1
- package/dist/config/schema.mjs +42 -42
- package/dist/config/schema.mjs.map +1 -1
- package/dist/gateway/server.d.mts.map +1 -1
- package/dist/gateway/server.mjs +48 -54
- package/dist/gateway/server.mjs.map +1 -1
- package/dist/heartbeat/service.d.mts.map +1 -1
- package/dist/heartbeat/service.mjs +8 -8
- package/dist/heartbeat/service.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/node_modules/{@jridgewell → .bun/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell}/sourcemap-codec/dist/sourcemap-codec.mjs +1 -1
- package/dist/node_modules/.bun/@jridgewell_sourcemap-codec@1.5.5/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_expect@2.1.9/node_modules/@vitest}/expect/dist/index.mjs +8 -8
- package/dist/node_modules/.bun/@vitest_expect@2.1.9/node_modules/@vitest/expect/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_pretty-format@2.1.9/node_modules/@vitest}/pretty-format/dist/index.mjs +2 -2
- package/dist/node_modules/.bun/@vitest_pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/chunk-tasks.mjs +1 -1
- package/dist/node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/chunk-tasks.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/index.mjs +6 -6
- package/dist/node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_snapshot@2.1.9/node_modules/@vitest}/snapshot/dist/index.mjs +5 -5
- package/dist/node_modules/.bun/@vitest_snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_spy@2.1.9/node_modules/@vitest}/spy/dist/index.mjs +2 -2
- package/dist/node_modules/.bun/@vitest_spy@2.1.9/node_modules/@vitest/spy/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/chunk-_commonjsHelpers.mjs +3 -3
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/diff.mjs +4 -4
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/diff.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/error.mjs +3 -3
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/error.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/helpers.mjs +1 -1
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/helpers.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/index.mjs +3 -3
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/index.mjs.map +1 -0
- package/dist/node_modules/{@vitest → .bun/@vitest_utils@2.1.9/node_modules/@vitest}/utils/dist/source-map.mjs +1 -1
- package/dist/node_modules/.bun/@vitest_utils@2.1.9/node_modules/@vitest/utils/dist/source-map.mjs.map +1 -0
- package/dist/node_modules/{chai → .bun/chai@5.3.3/node_modules/chai}/index.mjs +1 -1
- package/dist/node_modules/.bun/chai@5.3.3/node_modules/chai/index.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/arguments.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/arguments.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/array.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/array.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/bigint.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/bigint.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/class.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/class.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/date.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/date.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/error.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/error.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/function.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/function.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/helpers.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/helpers.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/html.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/html.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/index.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/index.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/map.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/map.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/number.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/number.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/object.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/object.mjs.map +1 -0
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/promise.mjs +6 -0
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/promise.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/regexp.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/regexp.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/set.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/set.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/string.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/string.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/symbol.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/symbol.mjs.map +1 -0
- package/dist/node_modules/{loupe → .bun/loupe@3.2.1/node_modules/loupe}/lib/typedarray.mjs +1 -1
- package/dist/node_modules/.bun/loupe@3.2.1/node_modules/loupe/lib/typedarray.mjs.map +1 -0
- package/dist/node_modules/{magic-string → .bun/magic-string@0.30.21/node_modules/magic-string}/dist/magic-string.es.mjs +2 -2
- package/dist/node_modules/.bun/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs.map +1 -0
- package/dist/node_modules/{@vitest/snapshot → .bun/pathe@1.1.2}/node_modules/pathe/dist/shared/pathe.ff20891b.mjs +1 -1
- package/dist/node_modules/.bun/pathe@1.1.2/node_modules/pathe/dist/shared/pathe.ff20891b.mjs.map +1 -0
- package/dist/node_modules/{tinyrainbow → .bun/tinyrainbow@1.2.0/node_modules/tinyrainbow}/dist/chunk-BVHSVHOK.mjs +1 -1
- package/dist/node_modules/.bun/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.mjs.map +1 -0
- package/dist/node_modules/{tinyrainbow → .bun/tinyrainbow@1.2.0/node_modules/tinyrainbow}/dist/node.mjs +1 -1
- package/dist/node_modules/.bun/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.mjs.map +1 -0
- package/dist/node_modules/{tinyspy → .bun/tinyspy@3.0.2/node_modules/tinyspy}/dist/index.mjs +1 -1
- package/dist/node_modules/.bun/tinyspy@3.0.2/node_modules/tinyspy/dist/index.mjs.map +1 -0
- package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs +1 -1
- package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs.map +1 -0
- package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/date.W2xKR2qe.mjs +1 -1
- package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/date.W2xKR2qe.mjs.map +1 -0
- package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/utils.C8RiOc4B.mjs +2 -2
- package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/utils.C8RiOc4B.mjs.map +1 -0
- package/dist/node_modules/{vitest → .bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest}/dist/chunks/vi.DgezovHB.mjs +11 -11
- package/dist/node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs.map +1 -0
- package/dist/providers/base.d.mts +2 -2
- package/dist/providers/base.d.mts.map +1 -1
- package/dist/providers/openai-provider.d.mts.map +1 -1
- package/dist/providers/openai-provider.mjs +10 -9
- package/dist/providers/openai-provider.mjs.map +1 -1
- package/dist/providers/registry.d.mts +1 -1
- package/dist/providers/registry.d.mts.map +1 -1
- package/dist/providers/registry.mjs +99 -99
- package/dist/providers/registry.mjs.map +1 -1
- package/dist/session/manager.d.mts +2 -2
- package/dist/session/manager.d.mts.map +1 -1
- package/dist/session/manager.mjs +18 -19
- package/dist/session/manager.mjs.map +1 -1
- package/dist/utils/helpers.d.mts.map +1 -1
- package/dist/utils/helpers.mjs.map +1 -1
- package/package.json +11 -11
- package/skills/cron/SKILL.md +12 -8
- package/skills/daily-summary/SKILL.md +4 -0
- package/skills/english/SKILL.md +21 -7
- package/skills/expense/SKILL.md +11 -7
- package/skills/fortune/SKILL.md +24 -20
- package/skills/habit/SKILL.md +2 -1
- package/skills/hydration/SKILL.md +3 -0
- package/skills/memory/SKILL.md +1 -0
- package/skills/mood/SKILL.md +10 -6
- package/skills/skill-creator/SKILL.md +3 -0
- package/skills/summarize/SKILL.md +1 -0
- package/skills/weather/SKILL.md +10 -8
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +0 -1
- package/dist/node_modules/@vitest/expect/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/pretty-format/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/runner/dist/chunk-tasks.mjs.map +0 -1
- package/dist/node_modules/@vitest/runner/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/snapshot/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/snapshot/node_modules/pathe/dist/shared/pathe.ff20891b.mjs.map +0 -1
- package/dist/node_modules/@vitest/spy/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/diff.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/error.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/helpers.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/index.mjs.map +0 -1
- package/dist/node_modules/@vitest/utils/dist/source-map.mjs.map +0 -1
- package/dist/node_modules/chai/index.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/arguments.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/array.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/bigint.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/class.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/date.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/error.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/function.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/helpers.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/html.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/index.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/map.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/number.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/object.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/promise.mjs +0 -6
- package/dist/node_modules/loupe/lib/promise.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/regexp.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/set.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/string.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/symbol.mjs.map +0 -1
- package/dist/node_modules/loupe/lib/typedarray.mjs.map +0 -1
- package/dist/node_modules/magic-string/dist/magic-string.es.mjs.map +0 -1
- package/dist/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.mjs.map +0 -1
- package/dist/node_modules/tinyrainbow/dist/node.mjs.map +0 -1
- package/dist/node_modules/tinyspy/dist/index.mjs.map +0 -1
- package/dist/node_modules/vitest/dist/chunks/_commonjsHelpers.BFTU3MAI.mjs.map +0 -1
- package/dist/node_modules/vitest/dist/chunks/date.W2xKR2qe.mjs.map +0 -1
- package/dist/node_modules/vitest/dist/chunks/utils.C8RiOc4B.mjs.map +0 -1
- package/dist/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs.map +0 -1
- /package/dist/node_modules/{@vitest → .bun/@vitest_runner@2.1.9/node_modules/@vitest}/runner/dist/utils.mjs +0 -0
package/dist/channels/line.mjs
CHANGED
|
@@ -44,22 +44,22 @@ var LineChannel = class extends BaseChannel {
|
|
|
44
44
|
const messages = this.parseMessage(text);
|
|
45
45
|
try {
|
|
46
46
|
const res = await fetch("https://api.line.me/v2/bot/message/reply", {
|
|
47
|
-
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
replyToken,
|
|
49
|
+
messages
|
|
50
|
+
}),
|
|
48
51
|
headers: {
|
|
49
52
|
"Content-Type": "application/json",
|
|
50
53
|
Authorization: `Bearer ${this.lineConfig.channelAccessToken}`
|
|
51
54
|
},
|
|
52
|
-
|
|
53
|
-
replyToken,
|
|
54
|
-
messages
|
|
55
|
-
})
|
|
55
|
+
method: "POST"
|
|
56
56
|
});
|
|
57
57
|
if (!res.ok) {
|
|
58
58
|
const err = await res.text();
|
|
59
59
|
console.error(`LINE reply failed (${res.status}): ${err}`);
|
|
60
60
|
}
|
|
61
|
-
} catch (
|
|
62
|
-
console.error("LINE reply error:",
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("LINE reply error:", error);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
/** Send a push message (works any time, consumes monthly quota). */
|
|
@@ -67,22 +67,22 @@ var LineChannel = class extends BaseChannel {
|
|
|
67
67
|
const messages = this.parseMessage(text);
|
|
68
68
|
try {
|
|
69
69
|
const res = await fetch("https://api.line.me/v2/bot/message/push", {
|
|
70
|
-
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
to,
|
|
72
|
+
messages
|
|
73
|
+
}),
|
|
71
74
|
headers: {
|
|
72
75
|
"Content-Type": "application/json",
|
|
73
76
|
Authorization: `Bearer ${this.lineConfig.channelAccessToken}`
|
|
74
77
|
},
|
|
75
|
-
|
|
76
|
-
to,
|
|
77
|
-
messages
|
|
78
|
-
})
|
|
78
|
+
method: "POST"
|
|
79
79
|
});
|
|
80
80
|
if (!res.ok) {
|
|
81
81
|
const err = await res.text();
|
|
82
82
|
console.error(`LINE push failed (${res.status}): ${err}`);
|
|
83
83
|
}
|
|
84
|
-
} catch (
|
|
85
|
-
console.error("LINE push error:",
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error("LINE push error:", error);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
@@ -94,7 +94,7 @@ var LineChannel = class extends BaseChannel {
|
|
|
94
94
|
if (event.mode !== "active") continue;
|
|
95
95
|
if (event.type === "message" && event.message) await this.onMessageEvent(event);
|
|
96
96
|
else if (event.type === "follow") {
|
|
97
|
-
const userId = event.source
|
|
97
|
+
const { userId } = event.source;
|
|
98
98
|
if (userId) {
|
|
99
99
|
console.log(`LINE: new follower ${userId}`);
|
|
100
100
|
if (event.replyToken) await this.reply(event.replyToken, "Hi! I'm nanobot. Send me a message and I'll respond!");
|
|
@@ -106,7 +106,7 @@ var LineChannel = class extends BaseChannel {
|
|
|
106
106
|
}
|
|
107
107
|
async onMessageEvent(event) {
|
|
108
108
|
const message = event.message;
|
|
109
|
-
const source = event
|
|
109
|
+
const { source } = event;
|
|
110
110
|
const senderId = source.userId ?? "unknown";
|
|
111
111
|
const chatId = source.type === "group" ? source.groupId ?? senderId : source.type === "room" ? source.roomId ?? senderId : senderId;
|
|
112
112
|
let content;
|
|
@@ -139,7 +139,6 @@ var LineChannel = class extends BaseChannel {
|
|
|
139
139
|
}
|
|
140
140
|
if (message.quotedMessageId) content = `[Quoting message ${message.quotedMessageId}]\n${content}`;
|
|
141
141
|
await this.handleMessage({
|
|
142
|
-
senderId,
|
|
143
142
|
chatId,
|
|
144
143
|
content,
|
|
145
144
|
media,
|
|
@@ -150,7 +149,8 @@ var LineChannel = class extends BaseChannel {
|
|
|
150
149
|
quoteToken: message.quoteToken,
|
|
151
150
|
...message.quotedMessageId && { quotedMessageId: message.quotedMessageId },
|
|
152
151
|
...message.mention && { mention: message.mention }
|
|
153
|
-
}
|
|
152
|
+
},
|
|
153
|
+
senderId
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
/** Build content string for a text message, including mention/emoji info. */
|
|
@@ -199,7 +199,7 @@ var LineChannel = class extends BaseChannel {
|
|
|
199
199
|
const parts = [];
|
|
200
200
|
if (message.title) parts.push(message.title);
|
|
201
201
|
if (message.address) parts.push(message.address);
|
|
202
|
-
if (message.latitude
|
|
202
|
+
if (message.latitude !== null && message.longitude !== null) parts.push(`(${message.latitude}, ${message.longitude})`);
|
|
203
203
|
return `[Location: ${parts.join(" | ")}]`;
|
|
204
204
|
}
|
|
205
205
|
/** Handle sticker message with resource type and keywords. */
|
|
@@ -229,8 +229,8 @@ var LineChannel = class extends BaseChannel {
|
|
|
229
229
|
writeFileSync(filePath, buffer);
|
|
230
230
|
console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);
|
|
231
231
|
return filePath;
|
|
232
|
-
} catch (
|
|
233
|
-
console.error("LINE content download error:",
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error("LINE content download error:", error);
|
|
234
234
|
return null;
|
|
235
235
|
}
|
|
236
236
|
}
|
|
@@ -249,12 +249,12 @@ var LineChannel = class extends BaseChannel {
|
|
|
249
249
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
250
250
|
const dir = this.workspace ? join(this.workspace, "uploads") : join(tmpdir(), "nanobot-line-uploads");
|
|
251
251
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
252
|
-
const filePath = join(dir, `${Date.now()}_${fileName.
|
|
252
|
+
const filePath = join(dir, `${Date.now()}_${fileName.replaceAll(/[^a-zA-Z0-9._-]/g, "_")}`);
|
|
253
253
|
writeFileSync(filePath, buffer);
|
|
254
254
|
console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);
|
|
255
255
|
return filePath;
|
|
256
|
-
} catch (
|
|
257
|
-
console.error("LINE file download error:",
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("LINE file download error:", error);
|
|
258
258
|
return null;
|
|
259
259
|
}
|
|
260
260
|
}
|
|
@@ -268,9 +268,9 @@ var LineChannel = class extends BaseChannel {
|
|
|
268
268
|
returnFlexMessage(message) {
|
|
269
269
|
const parsed = JSON.parse(message);
|
|
270
270
|
return [{
|
|
271
|
-
type: "flex",
|
|
272
271
|
altText: this.extractAltText(parsed),
|
|
273
|
-
contents: parsed
|
|
272
|
+
contents: parsed,
|
|
273
|
+
type: "flex"
|
|
274
274
|
}];
|
|
275
275
|
}
|
|
276
276
|
/** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */
|
|
@@ -280,7 +280,7 @@ var LineChannel = class extends BaseChannel {
|
|
|
280
280
|
return this.returnFlexMessage(trimmed);
|
|
281
281
|
} catch {}
|
|
282
282
|
const braceIdx = trimmed.indexOf("{");
|
|
283
|
-
if (braceIdx
|
|
283
|
+
if (braceIdx !== -1) {
|
|
284
284
|
const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));
|
|
285
285
|
if (jsonEnd > 0) {
|
|
286
286
|
const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);
|
|
@@ -289,21 +289,21 @@ var LineChannel = class extends BaseChannel {
|
|
|
289
289
|
try {
|
|
290
290
|
const messages = [];
|
|
291
291
|
if (prefix) messages.push({
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
text: prefix,
|
|
293
|
+
type: "text"
|
|
294
294
|
});
|
|
295
295
|
messages.push(...this.returnFlexMessage(jsonPart));
|
|
296
296
|
if (suffix) messages.push({
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
text: suffix,
|
|
298
|
+
type: "text"
|
|
299
299
|
});
|
|
300
300
|
return messages;
|
|
301
301
|
} catch {}
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
return [{
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
text: trimmed || "(empty)",
|
|
306
|
+
type: "text"
|
|
307
307
|
}];
|
|
308
308
|
}
|
|
309
309
|
/** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */
|
|
@@ -312,7 +312,7 @@ var LineChannel = class extends BaseChannel {
|
|
|
312
312
|
let depth = 0;
|
|
313
313
|
let inString = false;
|
|
314
314
|
let escape = false;
|
|
315
|
-
for (let i = 0; i < str.length; i
|
|
315
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
316
316
|
const ch = str[i];
|
|
317
317
|
if (escape) {
|
|
318
318
|
escape = false;
|
|
@@ -327,49 +327,49 @@ var LineChannel = class extends BaseChannel {
|
|
|
327
327
|
continue;
|
|
328
328
|
}
|
|
329
329
|
if (inString) continue;
|
|
330
|
-
if (ch === "{") depth
|
|
330
|
+
if (ch === "{") depth += 1;
|
|
331
331
|
else if (ch === "}") {
|
|
332
|
-
depth
|
|
332
|
+
depth -= 1;
|
|
333
333
|
if (depth === 0) return i + 1;
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
return -1;
|
|
337
337
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for (const item of obj) {
|
|
346
|
-
const result = extractText(item, depth + 1);
|
|
347
|
-
if (result) return result;
|
|
348
|
-
}
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
const record = obj;
|
|
352
|
-
if (record.text && typeof record.text === "string") return record.text.slice(0, 100);
|
|
353
|
-
if (record.title && typeof record.title === "string") return record.title.slice(0, 100);
|
|
354
|
-
for (const key of [
|
|
355
|
-
"contents",
|
|
356
|
-
"body",
|
|
357
|
-
"header",
|
|
358
|
-
"hero"
|
|
359
|
-
]) {
|
|
360
|
-
const result = extractText(record[key], depth + 1);
|
|
338
|
+
extractText = (obj, depth = 0) => {
|
|
339
|
+
if (depth > 5) return null;
|
|
340
|
+
if (typeof obj === "string") return obj.slice(0, 100);
|
|
341
|
+
if (!obj || typeof obj !== "object") return null;
|
|
342
|
+
if (Array.isArray(obj)) {
|
|
343
|
+
for (const item of obj) {
|
|
344
|
+
const result = this.extractText(item, depth + 1);
|
|
361
345
|
if (result) return result;
|
|
362
346
|
}
|
|
363
347
|
return null;
|
|
364
|
-
}
|
|
365
|
-
|
|
348
|
+
}
|
|
349
|
+
const record = obj;
|
|
350
|
+
if (record.text && typeof record.text === "string") return record.text.slice(0, 100);
|
|
351
|
+
if (record.title && typeof record.title === "string") return record.title.slice(0, 100);
|
|
352
|
+
for (const key of [
|
|
353
|
+
"contents",
|
|
354
|
+
"body",
|
|
355
|
+
"header",
|
|
356
|
+
"hero"
|
|
357
|
+
]) {
|
|
358
|
+
const result = this.extractText(record[key], depth + 1);
|
|
359
|
+
if (result) return result;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
};
|
|
363
|
+
/** Extract alt text from flex content. */
|
|
364
|
+
extractAltText(contents) {
|
|
365
|
+
return this.extractText(contents) || "Flex Message";
|
|
366
366
|
}
|
|
367
367
|
/** Format file size in human-readable form. */
|
|
368
368
|
formatFileSize(bytes) {
|
|
369
|
-
if (bytes
|
|
370
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
371
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
372
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
369
|
+
if (bytes === null) return "unknown size";
|
|
370
|
+
if (bytes !== void 0 && bytes < 1024) return `${bytes} B`;
|
|
371
|
+
if (bytes !== void 0 && bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
372
|
+
return `${((bytes ?? 0) / (1024 * 1024)).toFixed(1)} MB`;
|
|
373
373
|
}
|
|
374
374
|
};
|
|
375
375
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { tmpdir } from \"node:os\";\nimport type { messagingApi } from \"@line/bot-sdk\";\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport { BaseChannel } from \"./base.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string,\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ replyToken, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE reply error:\", err);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n body: JSON.stringify({ to, messages }),\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (err) {\n console.error(\"LINE push error:\", err);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") continue;\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const userId = event.source.userId;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\",\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(`LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n } else if (event.type === \"leave\") {\n console.log(`LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`);\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const source = event.source;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\":\n content = this.buildTextContent(message);\n break;\n\n case \"image\":\n content = await this.handleImageMessage(message, media);\n break;\n\n case \"video\":\n content = this.handleVideoMessage(message);\n break;\n\n case \"audio\":\n content = this.handleAudioMessage(message);\n break;\n\n case \"file\":\n content = await this.handleFileMessage(message);\n break;\n\n case \"location\":\n content = this.handleLocationMessage(message);\n break;\n\n case \"sticker\":\n content = this.handleStickerMessage(message);\n break;\n\n default:\n content = `[${message.type} message]`;\n break;\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n senderId,\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && { quotedMessageId: message.quotedMessageId }),\n ...(message.mention && { mention: message.mention }),\n },\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[],\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) parts.push(message.title);\n if (message.address) parts.push(message.address);\n if (message.latitude != null && message.longitude != null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: downloaded content ${messageId} (${buffer.length} bytes)`);\n return filePath;\n } catch (err) {\n console.error(\"LINE content download error:\", err);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string,\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n },\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replace(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(`LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`);\n return filePath;\n } catch (err) {\n console.error(\"LINE file download error:\", err);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature,\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [{\n type: \"flex\",\n altText: this.extractAltText(parsed),\n contents: parsed,\n }];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract a JSON object from anywhere in the string\n }\n\n // Find first '{' in the string and try to extract a JSON object\n const braceIdx = trimmed.indexOf(\"{\");\n if (braceIdx >= 0) {\n const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);\n const prefix = trimmed.slice(0, braceIdx).trim();\n const suffix = trimmed.slice(braceIdx + jsonEnd).trim();\n try {\n const messages: messagingApi.Message[] = [];\n if (prefix) {\n messages.push({ type: \"text\", text: prefix });\n }\n messages.push(...this.returnFlexMessage(jsonPart));\n if (suffix) {\n messages.push({ type: \"text\", text: suffix });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ type: \"text\", text: trimmed || \"(empty)\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") return -1;\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i++) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) escape = true;\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) continue;\n\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n depth--;\n if (depth === 0) return i + 1;\n }\n }\n\n return -1;\n }\n\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n const extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) return null;\n if (typeof obj === \"string\") return obj.slice(0, 100);\n if (!obj || typeof obj !== \"object\") return null;\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = extractText(item, depth + 1);\n if (result) return result;\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") return record.text.slice(0, 100);\n if (record.title && typeof record.title === \"string\") return record.title.slice(0, 100);\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = extractText(record[key], depth + 1);\n if (result) return result;\n }\n return null;\n };\n\n return extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes == null) return \"unknown size\";\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"line.mjs","names":[],"sources":["../../src/channels/line.ts"],"sourcesContent":["import type { messagingApi } from \"@line/bot-sdk\";\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\n\nimport type { OutboundMessage } from \"../bus/events.js\";\nimport type { MessageBus } from \"../bus/queue.js\";\nimport type { LineConfig } from \"../config/schema.js\";\n\nimport { BaseChannel } from \"./base.js\";\n\n// ---------------------------------------------------------------------------\n// LINE Messaging API types (webhook events)\n// ---------------------------------------------------------------------------\n\ninterface LineWebhookBody {\n destination: string;\n events: LineEvent[];\n}\n\ninterface LineSource {\n type: \"user\" | \"group\" | \"room\";\n userId?: string;\n groupId?: string;\n roomId?: string;\n}\n\ninterface LineEvent {\n type: string;\n timestamp: number;\n source: LineSource;\n replyToken?: string;\n mode: \"active\" | \"standby\";\n webhookEventId: string;\n deliveryContext: { isRedelivery: boolean };\n message?: LineMessage;\n}\n\n// -- Emoji in text messages ------------------------------------------------\n\ninterface LineEmoji {\n index: number;\n length: number;\n productId: string;\n emojiId: string;\n}\n\n// -- Mention in text messages ----------------------------------------------\n\ninterface LineMentionee {\n index: number;\n length: number;\n type: \"user\" | \"all\";\n userId?: string;\n isSelf?: boolean;\n}\n\ninterface LineMention {\n mentionees: LineMentionee[];\n}\n\n// -- Content provider (image, video, audio) --------------------------------\n\ninterface LineContentProvider {\n type: \"line\" | \"external\";\n originalContentUrl?: string;\n previewImageUrl?: string;\n}\n\n// -- Image set (multiple images sent simultaneously) -----------------------\n\ninterface LineImageSet {\n id: string;\n index?: number;\n total?: number;\n}\n\n// -- Union message type ----------------------------------------------------\n\ninterface LineMessage {\n type: string;\n id: string;\n quoteToken?: string;\n quotedMessageId?: string;\n\n // Text\n text?: string;\n emojis?: LineEmoji[];\n mention?: LineMention;\n\n // Image\n contentProvider?: LineContentProvider;\n imageSet?: LineImageSet;\n\n // Video / Audio\n duration?: number;\n\n // File\n fileName?: string;\n fileSize?: number;\n\n // Location\n title?: string;\n address?: string;\n latitude?: number;\n longitude?: number;\n\n // Sticker\n packageId?: string;\n stickerId?: string;\n stickerResourceType?: string;\n keywords?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Signature verification\n// ---------------------------------------------------------------------------\n\nexport function verifyLineSignature(\n channelSecret: string,\n rawBody: string,\n signature: string\n): boolean {\n const digest = createHmac(\"sha256\", channelSecret)\n .update(rawBody)\n .digest(\"base64\");\n try {\n return timingSafeEqual(Buffer.from(digest), Buffer.from(signature));\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// LINE Channel\n// ---------------------------------------------------------------------------\n\n/**\n * LINE Messaging API channel.\n *\n * Unlike Telegram (long-polling), LINE uses webhooks -- the HTTP server pushes\n * events into this channel via `handleWebhook()`. The channel is registered in\n * the gateway Hono server which forwards POST /webhook/line to it.\n */\nexport class LineChannel extends BaseChannel {\n readonly name = \"line\";\n private lineConfig: LineConfig;\n private workspace: string | null;\n\n constructor(config: LineConfig, bus: MessageBus, workspace?: string) {\n super(config, bus);\n this.lineConfig = config;\n this.workspace = workspace ?? null;\n }\n\n // LINE is webhook-driven; start/stop are no-ops.\n async start(): Promise<void> {\n this._running = true;\n console.log(\"LINE channel ready (webhook mode)\");\n }\n\n async stop(): Promise<void> {\n this._running = false;\n }\n\n // ------ Outbound: reply or push ------\n\n async send(msg: OutboundMessage): Promise<void> {\n // msg.chatId is the LINE userId (or groupId/roomId)\n // Reply tokens expire quickly, so we always use the push API\n // for outbound messages routed through the bus.\n await this.pushMessage(msg.chatId, msg.content);\n }\n\n /** Send a reply using a replyToken (fast, free, single-use). */\n async reply(replyToken: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/reply\", {\n body: JSON.stringify({ replyToken, messages }),\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n method: \"POST\",\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE reply failed (${res.status}): ${err}`);\n }\n } catch (error) {\n console.error(\"LINE reply error:\", error);\n }\n }\n\n /** Send a push message (works any time, consumes monthly quota). */\n async pushMessage(to: string, text: string): Promise<void> {\n const messages = this.parseMessage(text);\n try {\n const res = await fetch(\"https://api.line.me/v2/bot/message/push\", {\n body: JSON.stringify({ to, messages }),\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n method: \"POST\",\n });\n if (!res.ok) {\n const err = await res.text();\n console.error(`LINE push failed (${res.status}): ${err}`);\n }\n } catch (error) {\n console.error(\"LINE push error:\", error);\n }\n }\n\n // ------ Inbound: webhook events ------\n\n /**\n * Process a raw webhook request.\n * Called by the gateway Hono route after signature verification.\n */\n async handleWebhook(body: LineWebhookBody): Promise<void> {\n for (const event of body.events) {\n if (event.mode !== \"active\") {\n continue;\n }\n\n if (event.type === \"message\" && event.message) {\n await this.onMessageEvent(event);\n } else if (event.type === \"follow\") {\n const { userId } = event.source;\n if (userId) {\n console.log(`LINE: new follower ${userId}`);\n if (event.replyToken) {\n await this.reply(\n event.replyToken,\n \"Hi! I'm nanobot. Send me a message and I'll respond!\"\n );\n }\n }\n } else if (event.type === \"unfollow\") {\n console.log(`LINE: unfollowed by ${event.source.userId ?? \"unknown\"}`);\n } else if (event.type === \"join\") {\n console.log(\n `LINE: joined ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`\n );\n } else if (event.type === \"leave\") {\n console.log(\n `LINE: left ${event.source.type} ${event.source.groupId ?? event.source.roomId ?? \"?\"}`\n );\n }\n }\n }\n\n private async onMessageEvent(event: LineEvent): Promise<void> {\n const message = event.message!;\n const { source } = event;\n\n // Determine senderId and chatId\n const senderId = source.userId ?? \"unknown\";\n const chatId =\n source.type === \"group\"\n ? (source.groupId ?? senderId)\n : source.type === \"room\"\n ? (source.roomId ?? senderId)\n : senderId;\n\n // Build content and optional media\n let content: string;\n const media: string[] = [];\n\n switch (message.type) {\n case \"text\": {\n content = this.buildTextContent(message);\n break;\n }\n\n case \"image\": {\n content = await this.handleImageMessage(message, media);\n break;\n }\n\n case \"video\": {\n content = this.handleVideoMessage(message);\n break;\n }\n\n case \"audio\": {\n content = this.handleAudioMessage(message);\n break;\n }\n\n case \"file\": {\n content = await this.handleFileMessage(message);\n break;\n }\n\n case \"location\": {\n content = this.handleLocationMessage(message);\n break;\n }\n\n case \"sticker\": {\n content = this.handleStickerMessage(message);\n break;\n }\n\n default: {\n content = `[${message.type} message]`;\n break;\n }\n }\n\n // Prepend quoted message context if this message quotes a previous one\n if (message.quotedMessageId) {\n content = `[Quoting message ${message.quotedMessageId}]\\n${content}`;\n }\n\n // Store the replyToken + rich metadata so downstream can use them\n await this.handleMessage({\n chatId,\n content,\n media,\n metadata: {\n messageId: message.id,\n replyToken: event.replyToken,\n sourceType: source.type,\n quoteToken: message.quoteToken,\n ...(message.quotedMessageId && {\n quotedMessageId: message.quotedMessageId,\n }),\n ...(message.mention && { mention: message.mention }),\n },\n senderId,\n });\n }\n\n // ------ Message type handlers ------\n\n /** Build content string for a text message, including mention/emoji info. */\n private buildTextContent(message: LineMessage): string {\n let content = message.text ?? \"\";\n\n // If the text contains mentions, annotate them\n if (message.mention && message.mention.mentionees.length > 0) {\n const mentionInfo = message.mention.mentionees\n .map((m) => {\n const who = m.type === \"all\" ? \"@All\" : (m.userId ?? \"user\");\n const isSelf = m.isSelf ? \" (mentioning bot)\" : \"\";\n return `${who}${isSelf}`;\n })\n .join(\", \");\n content += `\\n[Mentions: ${mentionInfo}]`;\n }\n\n return content;\n }\n\n /** Handle image message: download from Content API if provider is LINE. */\n private async handleImageMessage(\n message: LineMessage,\n media: string[]\n ): Promise<string> {\n const provider = message.contentProvider;\n\n if (provider?.type === \"external\" && provider.originalContentUrl) {\n // External image: use the URL directly\n media.push(provider.originalContentUrl);\n return \"[User sent an image]\";\n }\n\n // LINE-hosted image: download via Content API\n const imagePath = await this.downloadContent(message.id, \"jpg\");\n if (imagePath) {\n media.push(imagePath);\n const setInfo = message.imageSet\n ? ` (${message.imageSet.index ?? \"?\"}/${message.imageSet.total ?? \"?\"})`\n : \"\";\n return `[User sent an image${setInfo}]`;\n }\n return \"[Image: failed to download]\";\n }\n\n /** Handle video message. */\n private handleVideoMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Video message${duration} -- video analysis not yet supported]`;\n }\n\n /** Handle audio message. */\n private handleAudioMessage(message: LineMessage): string {\n const duration = message.duration\n ? ` (${Math.round(message.duration / 1000)}s)`\n : \"\";\n return `[Audio message${duration} -- transcription not yet supported]`;\n }\n\n /** Handle file message: download from Content API to workspace. */\n private async handleFileMessage(message: LineMessage): Promise<string> {\n const fileName = message.fileName ?? `file_${message.id}`;\n const fileSize = this.formatFileSize(message.fileSize);\n\n const savedPath = await this.downloadToWorkspace(message.id, fileName);\n if (savedPath) {\n return `[File received: ${fileName} (${fileSize}) — saved to ${savedPath}]`;\n }\n return `[File: ${fileName} (${fileSize}) — download failed]`;\n }\n\n /** Handle location message with all available fields. */\n private handleLocationMessage(message: LineMessage): string {\n const parts: string[] = [];\n if (message.title) {\n parts.push(message.title);\n }\n if (message.address) {\n parts.push(message.address);\n }\n if (message.latitude !== null && message.longitude !== null) {\n parts.push(`(${message.latitude}, ${message.longitude})`);\n }\n return `[Location: ${parts.join(\" | \")}]`;\n }\n\n /** Handle sticker message with resource type and keywords. */\n private handleStickerMessage(message: LineMessage): string {\n const parts: string[] = [\"Sticker\"];\n\n // Include sticker text for message stickers\n if (message.text) {\n parts.push(`\"${message.text}\"`);\n }\n\n // Include keywords if available (gives the AI context about the sticker)\n if (message.keywords && message.keywords.length > 0) {\n parts.push(`(${message.keywords.join(\", \")})`);\n }\n\n // Resource type for logging\n const resType = message.stickerResourceType;\n if (resType && resType !== \"STATIC\") {\n parts.push(`[${resType.toLowerCase()}]`);\n }\n\n return `[${parts.join(\" \")}]`;\n }\n\n // ------ Content download ------\n\n /**\n * Download message content (image, video, audio, file) from LINE Content API.\n * Returns the local file path, or null on failure.\n */\n private async downloadContent(\n messageId: string,\n ext: string\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n }\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Save to temp dir\n const dir = join(tmpdir(), \"nanobot-line-media\");\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n const filePath = join(dir, `${messageId}.${ext}`);\n writeFileSync(filePath, buffer);\n\n console.log(\n `LINE: downloaded content ${messageId} (${buffer.length} bytes)`\n );\n return filePath;\n } catch (error) {\n console.error(\"LINE content download error:\", error);\n return null;\n }\n }\n\n /**\n * Download message content to the workspace uploads directory.\n * Falls back to tmpdir if no workspace is configured.\n * Returns the saved file path, or null on failure.\n */\n private async downloadToWorkspace(\n messageId: string,\n fileName: string\n ): Promise<string | null> {\n try {\n const res = await fetch(\n `https://api-data.line.me/v2/bot/message/${messageId}/content`,\n {\n headers: {\n Authorization: `Bearer ${this.lineConfig.channelAccessToken}`,\n },\n }\n );\n\n if (!res.ok) {\n console.error(`LINE content download failed (${res.status})`);\n return null;\n }\n\n const buffer = Buffer.from(await res.arrayBuffer());\n\n // Determine save directory\n const dir = this.workspace\n ? join(this.workspace, \"uploads\")\n : join(tmpdir(), \"nanobot-line-uploads\");\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Avoid filename collisions by prepending timestamp\n const safeName = `${Date.now()}_${fileName.replaceAll(/[^a-zA-Z0-9._-]/g, \"_\")}`;\n const filePath = join(dir, safeName);\n writeFileSync(filePath, buffer);\n\n console.log(\n `LINE: saved file ${fileName} (${buffer.length} bytes) -> ${filePath}`\n );\n return filePath;\n } catch (error) {\n console.error(\"LINE file download error:\", error);\n return null;\n }\n }\n\n // ------ Signature ------\n\n /**\n * Verify webhook signature.\n * Exposed as a helper so the gateway route can call it.\n */\n verifySignature(rawBody: string, signature: string): boolean {\n return verifyLineSignature(\n this.lineConfig.channelSecret,\n rawBody,\n signature\n );\n }\n\n // ------ Helpers ------\n\n private returnFlexMessage(message: string): messagingApi.Message[] {\n const parsed = JSON.parse(message);\n\n return [\n {\n altText: this.extractAltText(parsed),\n contents: parsed,\n type: \"flex\",\n },\n ];\n }\n\n /** Parse text into LINE message(s). If valid Flex JSON found, send as flex + surrounding text as separate messages. */\n private parseMessage(text: string): messagingApi.Message[] {\n const trimmed = text.trim();\n\n // Try parsing the entire string as JSON first\n try {\n return this.returnFlexMessage(trimmed);\n } catch {\n // Not pure JSON — try to extract a JSON object from anywhere in the string\n }\n\n // Find first '{' in the string and try to extract a JSON object\n const braceIdx = trimmed.indexOf(\"{\");\n if (braceIdx !== -1) {\n const jsonEnd = this.findJsonEnd(trimmed.slice(braceIdx));\n if (jsonEnd > 0) {\n const jsonPart = trimmed.slice(braceIdx, braceIdx + jsonEnd);\n const prefix = trimmed.slice(0, braceIdx).trim();\n const suffix = trimmed.slice(braceIdx + jsonEnd).trim();\n try {\n const messages: messagingApi.Message[] = [];\n if (prefix) {\n messages.push({ text: prefix, type: \"text\" });\n }\n messages.push(...this.returnFlexMessage(jsonPart));\n if (suffix) {\n messages.push({ text: suffix, type: \"text\" });\n }\n return messages;\n } catch {\n // JSON was invalid flex, fall through\n }\n }\n }\n\n return [{ text: trimmed || \"(empty)\", type: \"text\" }];\n }\n\n /** Find the end index of a top-level JSON object in a string. Returns -1 if not found. */\n private findJsonEnd(str: string): number {\n if (str[0] !== \"{\") {\n return -1;\n }\n let depth = 0;\n let inString = false;\n let escape = false;\n\n for (let i = 0; i < str.length; i += 1) {\n const ch = str[i];\n\n if (escape) {\n escape = false;\n continue;\n }\n\n if (ch === \"\\\\\") {\n if (inString) {\n escape = true;\n }\n continue;\n }\n\n if (ch === '\"') {\n inString = !inString;\n continue;\n }\n\n if (inString) {\n continue;\n }\n\n if (ch === \"{\") {\n depth += 1;\n } else if (ch === \"}\") {\n depth -= 1;\n if (depth === 0) {\n return i + 1;\n }\n }\n }\n\n return -1;\n }\n\n private extractText = (obj: unknown, depth = 0): string | null => {\n if (depth > 5) {\n return null;\n }\n if (typeof obj === \"string\") {\n return obj.slice(0, 100);\n }\n if (!obj || typeof obj !== \"object\") {\n return null;\n }\n\n // Handle arrays: iterate elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const result = this.extractText(item, depth + 1);\n if (result) {\n return result;\n }\n }\n return null;\n }\n\n const record = obj as Record<string, unknown>;\n if (record.text && typeof record.text === \"string\") {\n return record.text.slice(0, 100);\n }\n if (record.title && typeof record.title === \"string\") {\n return record.title.slice(0, 100);\n }\n\n for (const key of [\"contents\", \"body\", \"header\", \"hero\"]) {\n const result = this.extractText(record[key], depth + 1);\n if (result) {\n return result;\n }\n }\n return null;\n };\n /** Extract alt text from flex content. */\n private extractAltText(contents: Record<string, unknown>): string {\n return this.extractText(contents) || \"Flex Message\";\n }\n\n /** Format file size in human-readable form. */\n private formatFileSize(bytes?: number): string {\n if (bytes === null) {\n return \"unknown size\";\n }\n if (bytes !== undefined && bytes < 1024) {\n return `${bytes} B`;\n }\n if (bytes !== undefined && bytes < 1024 * 1024) {\n return `${(bytes / 1024).toFixed(1)} KB`;\n }\n return `${((bytes ?? 0) / (1024 * 1024)).toFixed(1)} MB`;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { __require } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
1
|
import { MessageBus } from "../bus/queue.mjs";
|
|
3
2
|
import { LineChannel, verifyLineSignature } from "./line.mjs";
|
|
4
|
-
import { describe, it } from "../node_modules/@vitest/runner/dist/index.mjs";
|
|
5
|
-
import { globalExpect } from "../node_modules/vitest/dist/chunks/vi.DgezovHB.mjs";
|
|
3
|
+
import { describe, it } from "../node_modules/.bun/@vitest_runner@2.1.9/node_modules/@vitest/runner/dist/index.mjs";
|
|
4
|
+
import { globalExpect } from "../node_modules/.bun/vitest@2.1.9_7700f9e9ace41f23/node_modules/vitest/dist/chunks/vi.DgezovHB.mjs";
|
|
5
|
+
import { createHmac } from "node:crypto";
|
|
6
6
|
|
|
7
7
|
//#region src/channels/line.test.ts
|
|
8
8
|
const ch = new LineChannel({
|
|
9
|
-
|
|
10
|
-
channelSecret: "test",
|
|
9
|
+
allowFrom: [],
|
|
11
10
|
channelAccessToken: "test",
|
|
12
|
-
|
|
11
|
+
channelSecret: "test",
|
|
12
|
+
enabled: false
|
|
13
13
|
}, new MessageBus());
|
|
14
14
|
describe("findJsonEnd", () => {
|
|
15
15
|
it("finds end of simple object", () => {
|
|
@@ -46,24 +46,24 @@ describe("findJsonEnd", () => {
|
|
|
46
46
|
describe("extractAltText", () => {
|
|
47
47
|
it("extracts text from a simple bubble", () => {
|
|
48
48
|
globalExpect(ch.extractAltText({
|
|
49
|
-
type: "bubble",
|
|
50
49
|
body: {
|
|
51
50
|
type: "box",
|
|
52
51
|
contents: [{
|
|
53
52
|
type: "text",
|
|
54
53
|
text: "Hello World"
|
|
55
54
|
}]
|
|
56
|
-
}
|
|
55
|
+
},
|
|
56
|
+
type: "bubble"
|
|
57
57
|
})).toBe("Hello World");
|
|
58
58
|
});
|
|
59
59
|
it("extracts title when present", () => {
|
|
60
60
|
globalExpect(ch.extractAltText({
|
|
61
|
-
type: "bubble",
|
|
62
|
-
title: "Card Title",
|
|
63
61
|
body: {
|
|
64
62
|
type: "box",
|
|
65
63
|
contents: []
|
|
66
|
-
}
|
|
64
|
+
},
|
|
65
|
+
title: "Card Title",
|
|
66
|
+
type: "bubble"
|
|
67
67
|
})).toBe("Card Title");
|
|
68
68
|
});
|
|
69
69
|
it("falls back to 'Flex Message' when no text found", () => {
|
|
@@ -72,15 +72,15 @@ describe("extractAltText", () => {
|
|
|
72
72
|
it("truncates long text to 100 chars", () => {
|
|
73
73
|
const longText = "x".repeat(200);
|
|
74
74
|
globalExpect(ch.extractAltText({
|
|
75
|
-
type: "bubble",
|
|
76
75
|
body: {
|
|
77
76
|
type: "box",
|
|
78
77
|
contents: [{
|
|
79
78
|
type: "text",
|
|
80
79
|
text: longText
|
|
81
80
|
}]
|
|
82
|
-
}
|
|
83
|
-
|
|
81
|
+
},
|
|
82
|
+
type: "bubble"
|
|
83
|
+
})).toHaveLength(100);
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
86
|
describe("parseMessage", () => {
|
|
@@ -97,7 +97,6 @@ describe("parseMessage", () => {
|
|
|
97
97
|
});
|
|
98
98
|
it("parses pure JSON flex message", () => {
|
|
99
99
|
const flex = JSON.stringify({
|
|
100
|
-
type: "bubble",
|
|
101
100
|
body: {
|
|
102
101
|
type: "box",
|
|
103
102
|
layout: "vertical",
|
|
@@ -105,7 +104,8 @@ describe("parseMessage", () => {
|
|
|
105
104
|
type: "text",
|
|
106
105
|
text: "Test"
|
|
107
106
|
}]
|
|
108
|
-
}
|
|
107
|
+
},
|
|
108
|
+
type: "bubble"
|
|
109
109
|
});
|
|
110
110
|
const result = ch.parseMessage(flex);
|
|
111
111
|
globalExpect(result).toHaveLength(1);
|
|
@@ -114,7 +114,6 @@ describe("parseMessage", () => {
|
|
|
114
114
|
});
|
|
115
115
|
it("handles JSON with trailing text", () => {
|
|
116
116
|
const input = JSON.stringify({
|
|
117
|
-
type: "bubble",
|
|
118
117
|
body: {
|
|
119
118
|
type: "box",
|
|
120
119
|
layout: "vertical",
|
|
@@ -122,7 +121,8 @@ describe("parseMessage", () => {
|
|
|
122
121
|
type: "text",
|
|
123
122
|
text: "Card"
|
|
124
123
|
}]
|
|
125
|
-
}
|
|
124
|
+
},
|
|
125
|
+
type: "bubble"
|
|
126
126
|
}) + "\n\nHere is some extra text!";
|
|
127
127
|
const result = ch.parseMessage(input);
|
|
128
128
|
globalExpect(result).toHaveLength(2);
|
|
@@ -132,7 +132,6 @@ describe("parseMessage", () => {
|
|
|
132
132
|
});
|
|
133
133
|
it("handles JSON with Japanese trailing text", () => {
|
|
134
134
|
const input = JSON.stringify({
|
|
135
|
-
type: "bubble",
|
|
136
135
|
body: {
|
|
137
136
|
type: "box",
|
|
138
137
|
layout: "vertical",
|
|
@@ -140,7 +139,8 @@ describe("parseMessage", () => {
|
|
|
140
139
|
type: "text",
|
|
141
140
|
text: "運勢"
|
|
142
141
|
}]
|
|
143
|
-
}
|
|
142
|
+
},
|
|
143
|
+
type: "bubble"
|
|
144
144
|
}) + "\n\n記録できた!今日もがんばろう。";
|
|
145
145
|
const result = ch.parseMessage(input);
|
|
146
146
|
globalExpect(result).toHaveLength(2);
|
|
@@ -150,14 +150,14 @@ describe("parseMessage", () => {
|
|
|
150
150
|
});
|
|
151
151
|
it("handles whitespace around JSON", () => {
|
|
152
152
|
const flex = JSON.stringify({
|
|
153
|
-
type: "bubble",
|
|
154
153
|
body: {
|
|
155
154
|
type: "box",
|
|
156
155
|
contents: [{
|
|
157
156
|
type: "text",
|
|
158
157
|
text: "OK"
|
|
159
158
|
}]
|
|
160
|
-
}
|
|
159
|
+
},
|
|
160
|
+
type: "bubble"
|
|
161
161
|
});
|
|
162
162
|
const result = ch.parseMessage(" " + flex + " ");
|
|
163
163
|
globalExpect(result).toHaveLength(1);
|
|
@@ -192,18 +192,17 @@ describe("parseMessage", () => {
|
|
|
192
192
|
globalExpect(result[1].text).toBe("記録できた!今日水やりしたことになってる。緑色のボタンがカワイイでしょ。");
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
-
describe("verifyLineSignature", () => {
|
|
195
|
+
describe("verifyLineSignature works", () => {
|
|
196
196
|
it("returns true for valid signature", () => {
|
|
197
197
|
const secret = "test-secret";
|
|
198
198
|
const body = "{\"events\":[]}";
|
|
199
|
-
|
|
200
|
-
globalExpect(verifyLineSignature(secret, body, createHmac("sha256", secret).update(body).digest("base64"))).toBe(true);
|
|
199
|
+
globalExpect(verifyLineSignature(secret, body, createHmac("sha256", secret).update(body).digest("base64"))).toBeTruthy();
|
|
201
200
|
});
|
|
202
201
|
it("returns false for invalid signature", () => {
|
|
203
|
-
globalExpect(verifyLineSignature("secret", "body", "bad-sig")).
|
|
202
|
+
globalExpect(verifyLineSignature("secret", "body", "bad-sig")).toBeFalsy();
|
|
204
203
|
});
|
|
205
204
|
it("returns false for empty signature", () => {
|
|
206
|
-
globalExpect(verifyLineSignature("secret", "body", "")).
|
|
205
|
+
globalExpect(verifyLineSignature("secret", "body", "")).toBeFalsy();
|
|
207
206
|
});
|
|
208
207
|
});
|
|
209
208
|
|