line_hono 0.1.1
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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/chunk-5V7NFQAQ.js +69 -0
- package/dist/index.js +404 -0
- package/dist/jsx.js +86 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Luis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 🔥 LINE Hono [](https://www.npmjs.com/package/line-hono)
|
|
2
|
+
|
|
3
|
+
**This library enables you to easily build LINE bots on Cloudflare Workers**
|
|
4
|
+
|
|
5
|
+
This project is influenced by [Hono](https://github.com/honojs/hono).
|
|
6
|
+
Thank you for [Yusuke Wada](https://github.com/yusukebe) and Hono contributors!
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Intuitive API** - Influenced by Hono, offering a familiar and easy-to-use interface.
|
|
11
|
+
- **Flattened Context** - Call `c.text()`, `c.flex()`, etc., directly on the context.
|
|
12
|
+
- **Functional Flex API** - Zero-overhead, pure functions for building Flex messages.
|
|
13
|
+
- **JSX Support** - Use JSX markup to design complex Flex messages.
|
|
14
|
+
- **Lightweight** - Minimal dependencies, optimized for edge environments.
|
|
15
|
+
- **Type-Safe** - Native support for LINE Messaging API types.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
npm i line-hono
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Docs
|
|
24
|
+
|
|
25
|
+
- [`docs/index.md`](./docs/index.md)
|
|
26
|
+
|
|
27
|
+
## Example Code
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { LineHono } from 'line-hono'
|
|
31
|
+
|
|
32
|
+
const app = new LineHono()
|
|
33
|
+
|
|
34
|
+
app.message('ping', (c) => c.text('pong'))
|
|
35
|
+
|
|
36
|
+
app.follow((c) => c.text('Thanks for following!'))
|
|
37
|
+
|
|
38
|
+
export default app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Functional Flex Message
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { bubble, box, text } from 'line-hono'
|
|
45
|
+
|
|
46
|
+
app.message('flex', (c) => {
|
|
47
|
+
return c.flex('Alt text', bubble({
|
|
48
|
+
body: box('vertical', [
|
|
49
|
+
text('Hello from line-hono!')
|
|
50
|
+
])
|
|
51
|
+
}))
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### JSX Flex Message (Optional)
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
/** @jsx jsx */
|
|
59
|
+
/** @jsxFrag Fragment */
|
|
60
|
+
import { jsx } from 'line-hono/jsx'
|
|
61
|
+
import { Bubble, Box, Text } from 'line-hono/jsx'
|
|
62
|
+
|
|
63
|
+
app.message('jsx', (c) => {
|
|
64
|
+
return c.flex('Alt text', (
|
|
65
|
+
<Bubble>
|
|
66
|
+
<Box layout="vertical">
|
|
67
|
+
<Text>Hello from JSX!</Text>
|
|
68
|
+
</Box>
|
|
69
|
+
</Bubble>
|
|
70
|
+
))
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Environment Variables
|
|
75
|
+
|
|
76
|
+
- `LINE_CHANNEL_ACCESS_TOKEN`: Channel Access Token from LINE Developers Console
|
|
77
|
+
- `LINE_CHANNEL_SECRET`: Channel Secret from LINE Developers Console
|
|
78
|
+
|
|
79
|
+
## References
|
|
80
|
+
|
|
81
|
+
- [Hono](https://github.com/honojs/hono) - [MIT License](https://github.com/honojs/hono/blob/main/LICENSE)
|
|
82
|
+
- [LINE Messaging API Documentation](https://developers.line.biz/en/docs/messaging-api/)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/flex.ts
|
|
2
|
+
var omit = (obj, keys) => {
|
|
3
|
+
const result = { ...obj };
|
|
4
|
+
for (const key of keys) {
|
|
5
|
+
if (key in result) delete result[key];
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
};
|
|
9
|
+
var bubble = (options = {}) => ({
|
|
10
|
+
type: "bubble",
|
|
11
|
+
...omit(options, ["children"])
|
|
12
|
+
});
|
|
13
|
+
var carousel = (contents, options = {}) => ({
|
|
14
|
+
type: "carousel",
|
|
15
|
+
contents,
|
|
16
|
+
...omit(options, ["children"])
|
|
17
|
+
});
|
|
18
|
+
var box = (layout, contents, options = {}) => ({
|
|
19
|
+
type: "box",
|
|
20
|
+
layout,
|
|
21
|
+
contents,
|
|
22
|
+
...omit(options, ["children"])
|
|
23
|
+
});
|
|
24
|
+
var text = (text2, options = {}) => ({
|
|
25
|
+
type: "text",
|
|
26
|
+
text: text2,
|
|
27
|
+
...omit(options, ["children"])
|
|
28
|
+
});
|
|
29
|
+
var button = (action, options = {}) => ({
|
|
30
|
+
type: "button",
|
|
31
|
+
action: typeof action === "string" ? { type: "uri", label: action, uri: action } : action,
|
|
32
|
+
...omit(options, ["children"])
|
|
33
|
+
});
|
|
34
|
+
var image = (url, options = {}) => ({
|
|
35
|
+
type: "image",
|
|
36
|
+
url,
|
|
37
|
+
...omit(options, ["children"])
|
|
38
|
+
});
|
|
39
|
+
var icon = (url, options = {}) => ({
|
|
40
|
+
type: "icon",
|
|
41
|
+
url,
|
|
42
|
+
...omit(options, ["children"])
|
|
43
|
+
});
|
|
44
|
+
var span = (text2, options = {}) => ({
|
|
45
|
+
type: "span",
|
|
46
|
+
text: text2,
|
|
47
|
+
...omit(options, ["children"])
|
|
48
|
+
});
|
|
49
|
+
var separator = (options = {}) => ({
|
|
50
|
+
type: "separator",
|
|
51
|
+
...omit(options, ["children"])
|
|
52
|
+
});
|
|
53
|
+
var filler = (options = {}) => ({
|
|
54
|
+
type: "filler",
|
|
55
|
+
...omit(options, ["children"])
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
bubble,
|
|
60
|
+
carousel,
|
|
61
|
+
box,
|
|
62
|
+
text,
|
|
63
|
+
button,
|
|
64
|
+
image,
|
|
65
|
+
icon,
|
|
66
|
+
span,
|
|
67
|
+
separator,
|
|
68
|
+
filler
|
|
69
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import {
|
|
2
|
+
box,
|
|
3
|
+
bubble,
|
|
4
|
+
button,
|
|
5
|
+
carousel,
|
|
6
|
+
filler,
|
|
7
|
+
icon,
|
|
8
|
+
image,
|
|
9
|
+
separator,
|
|
10
|
+
span,
|
|
11
|
+
text
|
|
12
|
+
} from "./chunk-5V7NFQAQ.js";
|
|
13
|
+
|
|
14
|
+
// src/context.ts
|
|
15
|
+
import { messagingApi } from "@line/bot-sdk";
|
|
16
|
+
var Context = class {
|
|
17
|
+
#env;
|
|
18
|
+
#executionCtx;
|
|
19
|
+
#line;
|
|
20
|
+
#event;
|
|
21
|
+
#var = /* @__PURE__ */ new Map();
|
|
22
|
+
#client;
|
|
23
|
+
constructor(env, executionCtx, line, event) {
|
|
24
|
+
this.#env = env;
|
|
25
|
+
this.#executionCtx = executionCtx;
|
|
26
|
+
this.#line = line;
|
|
27
|
+
this.#event = event;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Environment Variables
|
|
31
|
+
*/
|
|
32
|
+
get env() {
|
|
33
|
+
return this.#env;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Execution Context (for cloudflare workers, etc.)
|
|
37
|
+
*/
|
|
38
|
+
get executionCtx() {
|
|
39
|
+
return this.#executionCtx;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The raw LINE Webhook Event object.
|
|
43
|
+
*/
|
|
44
|
+
get event() {
|
|
45
|
+
return this.#event;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* ID of the user who sent the event.
|
|
49
|
+
*/
|
|
50
|
+
get userId() {
|
|
51
|
+
return "source" in this.#event && this.#event.source && "userId" in this.#event.source ? this.#event.source.userId : void 0;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* ID of the group where the event occurred.
|
|
55
|
+
*/
|
|
56
|
+
get groupId() {
|
|
57
|
+
return "source" in this.#event && this.#event.source && "groupId" in this.#event.source ? this.#event.source.groupId : void 0;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* ID of the room where the event occurred.
|
|
61
|
+
*/
|
|
62
|
+
get roomId() {
|
|
63
|
+
return "source" in this.#event && this.#event.source && "roomId" in this.#event.source ? this.#event.source.roomId : void 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Set a variable in the context.
|
|
67
|
+
*/
|
|
68
|
+
set(key, value) {
|
|
69
|
+
this.#var.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get a variable from the context.
|
|
73
|
+
*/
|
|
74
|
+
get(key) {
|
|
75
|
+
return this.#var.get(key);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* All variables in the context.
|
|
79
|
+
*/
|
|
80
|
+
get var() {
|
|
81
|
+
return Object.fromEntries(this.#var);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Official @line/bot-sdk MessagingApiClient instance.
|
|
85
|
+
*/
|
|
86
|
+
get client() {
|
|
87
|
+
if (!this.#client) {
|
|
88
|
+
if (!this.#line.CHANNEL_ACCESS_TOKEN) {
|
|
89
|
+
throw new Error("CHANNEL_ACCESS_TOKEN is required to initialize the LINE API client.");
|
|
90
|
+
}
|
|
91
|
+
this.#client = new messagingApi.MessagingApiClient({
|
|
92
|
+
channelAccessToken: this.#line.CHANNEL_ACCESS_TOKEN
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return this.#client;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reply to the current event.
|
|
99
|
+
* Usage: `c.reply(messages)` or `c.reply.text('Hello')`
|
|
100
|
+
*/
|
|
101
|
+
/*
|
|
102
|
+
* Aliases for reply methods
|
|
103
|
+
*/
|
|
104
|
+
get text() {
|
|
105
|
+
return this.reply.text;
|
|
106
|
+
}
|
|
107
|
+
get image() {
|
|
108
|
+
return this.reply.image;
|
|
109
|
+
}
|
|
110
|
+
get video() {
|
|
111
|
+
return this.reply.video;
|
|
112
|
+
}
|
|
113
|
+
get audio() {
|
|
114
|
+
return this.reply.audio;
|
|
115
|
+
}
|
|
116
|
+
get location() {
|
|
117
|
+
return this.reply.location;
|
|
118
|
+
}
|
|
119
|
+
get sticker() {
|
|
120
|
+
return this.reply.sticker;
|
|
121
|
+
}
|
|
122
|
+
get flex() {
|
|
123
|
+
return this.reply.flex;
|
|
124
|
+
}
|
|
125
|
+
get template() {
|
|
126
|
+
return this.reply.template;
|
|
127
|
+
}
|
|
128
|
+
get reply() {
|
|
129
|
+
const fn = async (messages) => {
|
|
130
|
+
if ("replyToken" in this.#event) {
|
|
131
|
+
const replyToken = this.#event.replyToken;
|
|
132
|
+
if (typeof replyToken === "string") {
|
|
133
|
+
return this.client.replyMessage({
|
|
134
|
+
replyToken,
|
|
135
|
+
messages: Array.isArray(messages) ? messages : [messages]
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error("This event does not support reply");
|
|
140
|
+
};
|
|
141
|
+
fn.text = (text2) => fn({ type: "text", text: text2 });
|
|
142
|
+
fn.image = (url, previewUrl) => fn({ type: "image", originalContentUrl: url, previewImageUrl: previewUrl || url });
|
|
143
|
+
fn.video = (url, previewUrl) => fn({ type: "video", originalContentUrl: url, previewImageUrl: previewUrl });
|
|
144
|
+
fn.audio = (url, duration) => fn({ type: "audio", originalContentUrl: url, duration });
|
|
145
|
+
fn.location = (title, address, lat, lon) => fn({ type: "location", title, address, latitude: lat, longitude: lon });
|
|
146
|
+
fn.sticker = (packageId, stickerId) => fn({ type: "sticker", packageId, stickerId });
|
|
147
|
+
fn.flex = (altText, contents) => {
|
|
148
|
+
return fn({ type: "flex", altText, contents });
|
|
149
|
+
};
|
|
150
|
+
fn.template = (altText, template) => fn({ type: "template", altText, template });
|
|
151
|
+
return fn;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Send a push message to the current source (user, group, or room).
|
|
155
|
+
* Usage: `c.push(messages)` or `c.push.text('Hello')`
|
|
156
|
+
*/
|
|
157
|
+
get push() {
|
|
158
|
+
const fn = async (messages) => {
|
|
159
|
+
const to = this.userId || this.groupId || this.roomId;
|
|
160
|
+
if (!to) throw new Error("No destination found for push");
|
|
161
|
+
return this.client.pushMessage({
|
|
162
|
+
to,
|
|
163
|
+
messages: Array.isArray(messages) ? messages : [messages]
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
fn.text = (text2) => fn({ type: "text", text: text2 });
|
|
167
|
+
fn.image = (url, previewUrl) => fn({ type: "image", originalContentUrl: url, previewImageUrl: previewUrl || url });
|
|
168
|
+
fn.video = (url, previewUrl) => fn({ type: "video", originalContentUrl: url, previewImageUrl: previewUrl });
|
|
169
|
+
fn.audio = (url, duration) => fn({ type: "audio", originalContentUrl: url, duration });
|
|
170
|
+
fn.location = (title, address, lat, lon) => fn({ type: "location", title, address, latitude: lat, longitude: lon });
|
|
171
|
+
fn.sticker = (packageId, stickerId) => fn({ type: "sticker", packageId, stickerId });
|
|
172
|
+
fn.flex = (altText, contents) => {
|
|
173
|
+
return fn({ type: "flex", altText, contents });
|
|
174
|
+
};
|
|
175
|
+
fn.template = (altText, template) => fn({ type: "template", altText, template });
|
|
176
|
+
return fn;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/utils.ts
|
|
181
|
+
var newError = (locate, text2) => new Error(`line-hono(${locate}): ${text2}`);
|
|
182
|
+
|
|
183
|
+
// src/verify.ts
|
|
184
|
+
var verify = async (body, signature, secret) => {
|
|
185
|
+
if (!body || !signature || !secret) return false;
|
|
186
|
+
const subtle = globalThis.crypto?.subtle;
|
|
187
|
+
if (!subtle) throw newError("verify", "crypto");
|
|
188
|
+
const encoder = new TextEncoder();
|
|
189
|
+
const keyData = encoder.encode(secret);
|
|
190
|
+
const key = await subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
191
|
+
const signatureBuffer = await subtle.sign("HMAC", key, encoder.encode(body));
|
|
192
|
+
const generatedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
|
|
193
|
+
return generatedSignature === signature;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/line-hono.ts
|
|
197
|
+
var LineHono = class {
|
|
198
|
+
#verify;
|
|
199
|
+
#line;
|
|
200
|
+
#handlers = /* @__PURE__ */ new Map();
|
|
201
|
+
#commands = /* @__PURE__ */ new Map();
|
|
202
|
+
#components = /* @__PURE__ */ new Map();
|
|
203
|
+
#modals = /* @__PURE__ */ new Map();
|
|
204
|
+
#crons = /* @__PURE__ */ new Map();
|
|
205
|
+
constructor(options) {
|
|
206
|
+
this.#verify = options?.verify ?? verify;
|
|
207
|
+
this.#line = (env) => {
|
|
208
|
+
const lineEnv = options?.lineEnv ? options.lineEnv(env) : {};
|
|
209
|
+
const bindings = env || {};
|
|
210
|
+
return {
|
|
211
|
+
CHANNEL_SECRET: lineEnv.CHANNEL_SECRET || bindings["LINE_CHANNEL_SECRET"],
|
|
212
|
+
CHANNEL_ACCESS_TOKEN: lineEnv.CHANNEL_ACCESS_TOKEN || bindings["LINE_CHANNEL_ACCESS_TOKEN"]
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a handler for a specific LINE event type.
|
|
218
|
+
* @param type Event type (e.g., 'message', 'follow', 'postback')
|
|
219
|
+
* @param handler Handler function
|
|
220
|
+
*/
|
|
221
|
+
on(type, handler) {
|
|
222
|
+
if (!this.#handlers.has(type)) {
|
|
223
|
+
this.#handlers.set(type, []);
|
|
224
|
+
}
|
|
225
|
+
this.#handlers.get(type)?.push(handler);
|
|
226
|
+
return this;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Register a handler for 'message' events.
|
|
230
|
+
*/
|
|
231
|
+
message(handler) {
|
|
232
|
+
return this.on("message", handler);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Register a handler for 'follow' events.
|
|
236
|
+
*/
|
|
237
|
+
follow(handler) {
|
|
238
|
+
return this.on("follow", handler);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Register a handler for 'unfollow' events.
|
|
242
|
+
*/
|
|
243
|
+
unfollow(handler) {
|
|
244
|
+
return this.on("unfollow", handler);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Register a handler for 'postback' events.
|
|
248
|
+
*/
|
|
249
|
+
postback(handler) {
|
|
250
|
+
return this.on("postback", handler);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Register a handler for 'join' events.
|
|
254
|
+
*/
|
|
255
|
+
join(handler) {
|
|
256
|
+
return this.on("join", handler);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Register a handler for 'leave' events.
|
|
260
|
+
*/
|
|
261
|
+
leave(handler) {
|
|
262
|
+
return this.on("leave", handler);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Register a handler for 'beacon' events.
|
|
266
|
+
*/
|
|
267
|
+
beacon(handler) {
|
|
268
|
+
return this.on("beacon", handler);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Register high-level handlers from a factory.
|
|
272
|
+
*/
|
|
273
|
+
loader(handlers) {
|
|
274
|
+
for (const h of handlers) {
|
|
275
|
+
if (h.type === "command") this.command(h.command.name, h.handler);
|
|
276
|
+
else if (h.type === "component") this.component(h.component.id, h.handler);
|
|
277
|
+
else if (h.type === "modal") this.modal(h.modal.id, h.handler);
|
|
278
|
+
else if (h.type === "cron") this.cron(h.cron, h.handler);
|
|
279
|
+
else throw new Error(`Unknown handler type: ${JSON.stringify(h)}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
command(name, handler) {
|
|
283
|
+
this.#commands.set(name, handler);
|
|
284
|
+
return this;
|
|
285
|
+
}
|
|
286
|
+
component(id, handler) {
|
|
287
|
+
this.#components.set(id, handler);
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
modal(id, handler) {
|
|
291
|
+
this.#modals.set(id, handler);
|
|
292
|
+
return this;
|
|
293
|
+
}
|
|
294
|
+
cron(expression, handler) {
|
|
295
|
+
this.#crons.set(expression, handler);
|
|
296
|
+
return this;
|
|
297
|
+
}
|
|
298
|
+
fetch = async (request, env, executionCtx) => {
|
|
299
|
+
if (request.method === "GET") {
|
|
300
|
+
return new Response("Operational\u{1F525}");
|
|
301
|
+
}
|
|
302
|
+
if (request.method !== "POST") {
|
|
303
|
+
return new Response("Not Found", { status: 404 });
|
|
304
|
+
}
|
|
305
|
+
const line = this.#line(env);
|
|
306
|
+
if (!line.CHANNEL_SECRET) throw newError("LineHono", "LINE_CHANNEL_SECRET");
|
|
307
|
+
const body = await request.text();
|
|
308
|
+
const signature = request.headers.get("x-line-signature");
|
|
309
|
+
if (!await this.#verify(body, signature, line.CHANNEL_SECRET)) {
|
|
310
|
+
return new Response("Unauthorized", { status: 401 });
|
|
311
|
+
}
|
|
312
|
+
const data = JSON.parse(body);
|
|
313
|
+
const promises = data.events.flatMap((event) => {
|
|
314
|
+
const handlers = this.#handlers.get(event.type) || [];
|
|
315
|
+
return handlers.map(async (handler) => {
|
|
316
|
+
const c = new Context(env, executionCtx, line, event);
|
|
317
|
+
return handler(c);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
if (executionCtx?.waitUntil) {
|
|
321
|
+
executionCtx.waitUntil(Promise.all(promises));
|
|
322
|
+
} else {
|
|
323
|
+
await Promise.all(promises);
|
|
324
|
+
}
|
|
325
|
+
return new Response("OK");
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/builders.ts
|
|
330
|
+
var Command = class {
|
|
331
|
+
name;
|
|
332
|
+
description;
|
|
333
|
+
constructor(name, description) {
|
|
334
|
+
this.name = name;
|
|
335
|
+
this.description = description;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var Button = class {
|
|
339
|
+
id;
|
|
340
|
+
label;
|
|
341
|
+
constructor(id, label) {
|
|
342
|
+
this.id = id;
|
|
343
|
+
this.label = label;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var Modal = class {
|
|
347
|
+
id;
|
|
348
|
+
title;
|
|
349
|
+
constructor(id, title) {
|
|
350
|
+
this.id = id;
|
|
351
|
+
this.title = title;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/helpers/create-factory.ts
|
|
356
|
+
var createFactory = () => {
|
|
357
|
+
return {
|
|
358
|
+
line: () => new LineHono(),
|
|
359
|
+
command: (name, description, handler) => ({
|
|
360
|
+
type: "command",
|
|
361
|
+
command: new Command(name, description),
|
|
362
|
+
handler
|
|
363
|
+
}),
|
|
364
|
+
component: (id, label, handler) => ({
|
|
365
|
+
type: "component",
|
|
366
|
+
component: new Button(id, label),
|
|
367
|
+
handler
|
|
368
|
+
}),
|
|
369
|
+
autocomplete: (command, autocomplete, handler) => ({
|
|
370
|
+
type: "autocomplete",
|
|
371
|
+
command,
|
|
372
|
+
autocomplete,
|
|
373
|
+
handler
|
|
374
|
+
}),
|
|
375
|
+
modal: (id, title, handler) => ({
|
|
376
|
+
type: "modal",
|
|
377
|
+
modal: new Modal(id, title),
|
|
378
|
+
handler
|
|
379
|
+
}),
|
|
380
|
+
cron: (cron, handler) => ({ type: "cron", cron, handler }),
|
|
381
|
+
getCommands: (handlers) => {
|
|
382
|
+
return handlers.filter((h) => h.type === "command").map((h) => h.command);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
export {
|
|
387
|
+
Button,
|
|
388
|
+
Command,
|
|
389
|
+
Context,
|
|
390
|
+
LineHono,
|
|
391
|
+
Modal,
|
|
392
|
+
box,
|
|
393
|
+
bubble,
|
|
394
|
+
button,
|
|
395
|
+
carousel,
|
|
396
|
+
createFactory,
|
|
397
|
+
filler,
|
|
398
|
+
icon,
|
|
399
|
+
image,
|
|
400
|
+
newError,
|
|
401
|
+
separator,
|
|
402
|
+
span,
|
|
403
|
+
text
|
|
404
|
+
};
|
package/dist/jsx.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
box,
|
|
3
|
+
bubble,
|
|
4
|
+
button,
|
|
5
|
+
carousel,
|
|
6
|
+
filler,
|
|
7
|
+
icon,
|
|
8
|
+
image,
|
|
9
|
+
separator,
|
|
10
|
+
span,
|
|
11
|
+
text
|
|
12
|
+
} from "./chunk-5V7NFQAQ.js";
|
|
13
|
+
|
|
14
|
+
// src/jsx.ts
|
|
15
|
+
var jsx = (tag, props, ...children) => {
|
|
16
|
+
if (typeof tag === "function") {
|
|
17
|
+
return tag({ ...props, children: children.length === 1 ? children[0] : children });
|
|
18
|
+
}
|
|
19
|
+
const flattenedChildren = children.flat(Infinity).filter(Boolean);
|
|
20
|
+
const p = props || {};
|
|
21
|
+
switch (tag.toLowerCase()) {
|
|
22
|
+
case "bubble":
|
|
23
|
+
return bubble({ ...p, body: flattenedChildren.find((c) => c.type === "box") });
|
|
24
|
+
case "carousel":
|
|
25
|
+
return carousel(flattenedChildren);
|
|
26
|
+
case "box":
|
|
27
|
+
return box(p["layout"] || "vertical", flattenedChildren, p);
|
|
28
|
+
case "text":
|
|
29
|
+
return text(p["text"] || (typeof children[0] === "string" ? children[0] : ""), p);
|
|
30
|
+
case "button":
|
|
31
|
+
return button(p["action"], p);
|
|
32
|
+
case "image":
|
|
33
|
+
return image(p["url"], p);
|
|
34
|
+
case "icon":
|
|
35
|
+
return icon(p["url"], p);
|
|
36
|
+
case "span":
|
|
37
|
+
return span(p["text"] || (typeof children[0] === "string" ? children[0] : ""), p);
|
|
38
|
+
case "separator":
|
|
39
|
+
return separator(p);
|
|
40
|
+
case "filler":
|
|
41
|
+
return filler(p);
|
|
42
|
+
default:
|
|
43
|
+
return { type: tag, ...p, contents: flattenedChildren };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var h = jsx;
|
|
47
|
+
var Bubble = (props) => {
|
|
48
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
49
|
+
const body = children.find((c) => c?.type === "box");
|
|
50
|
+
return bubble({ ...props, body });
|
|
51
|
+
};
|
|
52
|
+
var Carousel = (props) => {
|
|
53
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
54
|
+
return carousel(children, props);
|
|
55
|
+
};
|
|
56
|
+
var Box = (props) => {
|
|
57
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
58
|
+
return box(props.layout || "vertical", children, props);
|
|
59
|
+
};
|
|
60
|
+
var Text = (props) => {
|
|
61
|
+
const textContent = props.text || (typeof props.children === "string" ? props.children : void 0);
|
|
62
|
+
return text(textContent, props);
|
|
63
|
+
};
|
|
64
|
+
var Button = (props) => button(props.action, props);
|
|
65
|
+
var Image = (props) => image(props.url, props);
|
|
66
|
+
var Icon = (props) => icon(props.url, props);
|
|
67
|
+
var Span = (props) => {
|
|
68
|
+
const textContent = props.text || (typeof props.children === "string" ? props.children : void 0);
|
|
69
|
+
return span(textContent, props);
|
|
70
|
+
};
|
|
71
|
+
var Separator = (props) => separator(props);
|
|
72
|
+
var Filler = (props) => filler(props);
|
|
73
|
+
export {
|
|
74
|
+
Box,
|
|
75
|
+
Bubble,
|
|
76
|
+
Button,
|
|
77
|
+
Carousel,
|
|
78
|
+
Filler,
|
|
79
|
+
Icon,
|
|
80
|
+
Image,
|
|
81
|
+
Separator,
|
|
82
|
+
Span,
|
|
83
|
+
Text,
|
|
84
|
+
h,
|
|
85
|
+
jsx
|
|
86
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "line_hono",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "This library enables you to easily build LINE bots on Cloudflare Workers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"line-hono",
|
|
8
|
+
"line-bot",
|
|
9
|
+
"cloudflare-workers"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"module": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./jsx": {
|
|
24
|
+
"types": "./dist/jsx.d.ts",
|
|
25
|
+
"import": "./dist/jsx.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.4.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
33
|
+
"@biomejs/biome": "^2.4.4",
|
|
34
|
+
"@tsconfig/strictest": "^2.0.8",
|
|
35
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
36
|
+
"hono": "^4.12.3",
|
|
37
|
+
"mitata": "^1.0.34",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vitest": "^4.0.18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@line/bot-sdk": "^10.6.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"fix": "biome check --write .",
|
|
47
|
+
"fix:unsafe": "biome check --write --unsafe .",
|
|
48
|
+
"test": "biome check . && tsc && vitest run --coverage",
|
|
49
|
+
"build": "tsup src/index.ts src/jsx.ts --format esm --dts --clean && attw -P . --ignore-rules cjs-resolves-to-esm"
|
|
50
|
+
}
|
|
51
|
+
}
|