@mulmobridge/line 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/README.md +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +126 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @mulmobridge/line
|
|
2
|
+
|
|
3
|
+
> **Experimental** — please test and [report issues](https://github.com/receptron/mulmoclaude/issues/new).
|
|
4
|
+
|
|
5
|
+
LINE bridge for [MulmoClaude](https://github.com/receptron/mulmoclaude). Uses webhook events — requires a public URL (ngrok for development).
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### 1. Create a LINE Messaging API Channel
|
|
10
|
+
|
|
11
|
+
1. Go to [LINE Developers Console](https://developers.line.biz/console/) → create a Provider → create a **Messaging API** channel
|
|
12
|
+
2. Note the **Channel secret** (Basic settings tab)
|
|
13
|
+
3. Issue a **Channel access token** (Messaging API tab → long-lived)
|
|
14
|
+
|
|
15
|
+
### 2. Set up ngrok (for development)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ngrok http 3002
|
|
19
|
+
# Copy the https://xxxx.ngrok-free.app URL
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 3. Configure the webhook
|
|
23
|
+
|
|
24
|
+
In the LINE Developers Console → Messaging API tab:
|
|
25
|
+
- **Webhook URL**: `https://xxxx.ngrok-free.app/webhook`
|
|
26
|
+
- **Use webhook**: enabled
|
|
27
|
+
- **Auto-reply messages**: disabled (so LINE's default replies don't interfere)
|
|
28
|
+
|
|
29
|
+
### 4. Run the bridge
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# With mock server (testing)
|
|
33
|
+
npx @mulmobridge/mock-server &
|
|
34
|
+
LINE_CHANNEL_SECRET=... \
|
|
35
|
+
LINE_CHANNEL_ACCESS_TOKEN=... \
|
|
36
|
+
MULMOCLAUDE_AUTH_TOKEN=mock-test-token \
|
|
37
|
+
npx @mulmobridge/line
|
|
38
|
+
|
|
39
|
+
# With real MulmoClaude
|
|
40
|
+
LINE_CHANNEL_SECRET=... \
|
|
41
|
+
LINE_CHANNEL_ACCESS_TOKEN=... \
|
|
42
|
+
npx @mulmobridge/line
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 5. Add the bot as a friend
|
|
46
|
+
|
|
47
|
+
Scan the QR code in the LINE Developers Console → Messaging API tab. Send a message — MulmoClaude replies.
|
|
48
|
+
|
|
49
|
+
## Environment Variables
|
|
50
|
+
|
|
51
|
+
| Variable | Required | Description |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `LINE_CHANNEL_SECRET` | Yes | Channel secret for signature verification |
|
|
54
|
+
| `LINE_CHANNEL_ACCESS_TOKEN` | Yes | Long-lived channel access token |
|
|
55
|
+
| `LINE_BRIDGE_PORT` | No | Webhook port (default: 3002) |
|
|
56
|
+
| `MULMOCLAUDE_API_URL` | No | Default `http://localhost:3001` |
|
|
57
|
+
| `MULMOCLAUDE_AUTH_TOKEN` | No | Bearer token |
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- LINE reply tokens expire in **1 minute**. Since Claude responses can take longer, the bridge uses **push messages** instead of reply messages. This requires your bot to be a verified/certified account for push to work with all users, OR the user must have added the bot as a friend first.
|
|
62
|
+
- LINE limits messages to 5 per push call and ~5000 chars per message. Long replies are automatically chunked.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @mulmobridge/line — LINE bridge for MulmoClaude.
|
|
3
|
+
//
|
|
4
|
+
// Runs a small HTTP server to receive LINE webhook events.
|
|
5
|
+
// Requires a public URL (use ngrok for development).
|
|
6
|
+
//
|
|
7
|
+
// Required env vars:
|
|
8
|
+
// LINE_CHANNEL_SECRET — Channel secret from LINE Developers Console
|
|
9
|
+
// LINE_CHANNEL_ACCESS_TOKEN — Channel access token (long-lived)
|
|
10
|
+
//
|
|
11
|
+
// Optional:
|
|
12
|
+
// LINE_BRIDGE_PORT — Webhook listener port (default: 3002)
|
|
13
|
+
// MULMOCLAUDE_API_URL — default http://localhost:3001
|
|
14
|
+
// MULMOCLAUDE_AUTH_TOKEN — bearer token
|
|
15
|
+
import "dotenv/config";
|
|
16
|
+
import crypto from "crypto";
|
|
17
|
+
import express from "express";
|
|
18
|
+
import { createBridgeClient } from "@mulmobridge/client";
|
|
19
|
+
const TRANSPORT_ID = "line";
|
|
20
|
+
const PORT = Number(process.env.LINE_BRIDGE_PORT) || 3002;
|
|
21
|
+
const channelSecret = process.env.LINE_CHANNEL_SECRET;
|
|
22
|
+
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN;
|
|
23
|
+
if (!channelSecret || !channelAccessToken) {
|
|
24
|
+
console.error("LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN are required.\n" +
|
|
25
|
+
"See README for setup instructions.");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const client = createBridgeClient({ transportId: TRANSPORT_ID });
|
|
29
|
+
client.onPush((ev) => {
|
|
30
|
+
pushMessage(ev.chatId, ev.message).catch((err) => console.error(`[line] push send failed: ${err}`));
|
|
31
|
+
});
|
|
32
|
+
// ── LINE API helpers ────────────────────────────────────────────
|
|
33
|
+
async function pushMessage(userId, text) {
|
|
34
|
+
const messages = chunkText(text).map((t) => ({ type: "text", text: t }));
|
|
35
|
+
// LINE allows max 5 messages per push
|
|
36
|
+
for (let i = 0; i < messages.length; i += 5) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch("https://api.line.me/v2/bot/message/push", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Bearer ${channelAccessToken}`,
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
to: userId,
|
|
46
|
+
messages: messages.slice(i, i + 5),
|
|
47
|
+
}),
|
|
48
|
+
signal: AbortSignal.timeout(30_000),
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const body = await res.text().catch(() => "");
|
|
52
|
+
console.error(`[line] pushMessage failed: ${res.status} ${body.slice(0, 200)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error(`[line] pushMessage network error: ${err}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function chunkText(text, max = 5000) {
|
|
61
|
+
if (text.length === 0)
|
|
62
|
+
return ["(empty reply)"];
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for (let i = 0; i < text.length; i += max) {
|
|
65
|
+
chunks.push(text.slice(i, i + max));
|
|
66
|
+
}
|
|
67
|
+
return chunks;
|
|
68
|
+
}
|
|
69
|
+
// ── Signature verification ──────────────────────────────────────
|
|
70
|
+
function verifySignature(body, signature) {
|
|
71
|
+
const expected = crypto
|
|
72
|
+
.createHmac("SHA256", channelSecret)
|
|
73
|
+
.update(body)
|
|
74
|
+
.digest("base64");
|
|
75
|
+
if (expected.length !== signature.length)
|
|
76
|
+
return false;
|
|
77
|
+
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
78
|
+
}
|
|
79
|
+
// ── Webhook server ──────────────────────────────────────────────
|
|
80
|
+
const app = express();
|
|
81
|
+
app.disable("x-powered-by");
|
|
82
|
+
app.use(express.text({ type: "application/json" }));
|
|
83
|
+
app.post("/webhook", async (req, res) => {
|
|
84
|
+
const signature = req.headers["x-line-signature"];
|
|
85
|
+
const bodyStr = req.body;
|
|
86
|
+
if (!signature || !verifySignature(bodyStr, signature)) {
|
|
87
|
+
res.status(401).send("Invalid signature");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
res.status(200).send("OK");
|
|
91
|
+
let body;
|
|
92
|
+
try {
|
|
93
|
+
body = JSON.parse(bodyStr);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
console.error("[line] malformed JSON in webhook body");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const event of body.events) {
|
|
100
|
+
if (event.type !== "message" || event.message?.type !== "text")
|
|
101
|
+
continue;
|
|
102
|
+
const userId = event.source?.userId;
|
|
103
|
+
const text = event.message?.text ?? "";
|
|
104
|
+
if (!userId || !text.trim())
|
|
105
|
+
continue;
|
|
106
|
+
console.log(`[line] message user=${userId} len=${text.length}`);
|
|
107
|
+
try {
|
|
108
|
+
const ack = await client.send(userId, text);
|
|
109
|
+
if (ack.ok) {
|
|
110
|
+
await pushMessage(userId, ack.reply ?? "");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const status = ack.status ? ` (${ack.status})` : "";
|
|
114
|
+
await pushMessage(userId, `Error${status}: ${ack.error ?? "unknown"}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.error(`[line] message handling failed: ${err}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
app.listen(PORT, () => {
|
|
123
|
+
console.log("MulmoClaude LINE bridge");
|
|
124
|
+
console.log(`Webhook listening on http://localhost:${PORT}/webhook`);
|
|
125
|
+
console.log("Set your LINE webhook URL to: <public-url>/webhook");
|
|
126
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mulmobridge/line",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LINE bridge for MulmoBridge — connect a LINE bot to MulmoClaude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mulmobridge-line": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepack": "yarn build",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "echo no tests yet"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Receptron Team",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mulmobridge/client": "^0.1.0",
|
|
25
|
+
"@mulmobridge/protocol": "^0.1.0",
|
|
26
|
+
"dotenv": "^17.0.0",
|
|
27
|
+
"express": "^5.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/express": "^5.0.0",
|
|
31
|
+
"@types/node": "^25.6.0",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
}
|
|
34
|
+
}
|