node-red-contrib-line-messaging-api 0.1.8 → 0.1.9

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 CHANGED
@@ -7,10 +7,7 @@ LINE Messagin APIを利用できるNode-REDのノードです。
7
7
  以下のAPIを利用できます。
8
8
 
9
9
  - LINE Messaging API
10
- - Reply Message
11
- - text
12
- - image
13
- - flex
10
+ - Webhook & Reply Message
14
11
  - Push Message
15
12
  - BloadCast Message
16
13
  - LINE Notify
@@ -27,38 +24,13 @@ AdminタブからInstall
27
24
 
28
25
  ## 利用イメージ
29
26
 
30
- ### Reply Message
27
+ ### Webhook & Reply Message
31
28
 
32
- - 1. HTTP inノードで受け取る
33
-
34
- postで受けます。
35
-
36
- - 2. Functionノードでハンドリング
37
-
38
- ■テキスト例
39
-
40
- ```js
41
- if (msg.payload.events[0].message.text == '夏') {
42
- // 「夏」を受信したら「暑い」と返事する
43
- msg.payload.events[0].message.text = '暑い';
44
- } else {
45
- // それ以外を受信したら「わかりません」と返事する
46
- msg.payload.events[0].message.text = 'わかりません';
47
- }
48
- return msg;
49
- ```
50
-
51
- ■画像例
52
-
53
- ```js
54
- msg.payload.events[0].message.originalContentUrl = `https://pbs.twimg.com/profile_images/1165566424699457537/IYBnJ1i5_400x400.jpg`
55
-
56
- return msg;
57
- ```
58
-
59
- - 3. Reply Messageノードでリプライ
60
-
61
- ![](https://i.gyazo.com/d3df3a28e010b008043ed80ae6a672ea.gif)
29
+ 1. Webhookノードを配置し、ダブルクリックで設定を開き、指定した `/path` と自身のホスト名の組み合わせ(Webhook URL)を、LINE Developersであらかじめ作成したMessaging APIに登録します。
30
+ 2. ReplyMessageノードを配置し、チャネルのシークレットとアクセストークンを設定します。
31
+ 3. WebhookノードとReplyMessageノードを接続してLINEにメッセージを送るとオウム返しBotができます。
32
+ [![Image from Gyazo](https://i.gyazo.com/7da2dbecfc69515edf852cf7a26d9196.gif)](https://gyazo.com/7da2dbecfc69515edf852cf7a26d9196)
33
+ 4. WebhookノードとReplyMessageノードの中間で `msg.payload` をうまく作成すると様々なメッセージが送れます。文字列を指定すると通常のテキストメッセージに、[LINEで定義されているメッセージオブジェクト](https://developers.line.biz/ja/reference/messaging-api/#message-objects)を指定すればそのメッセージを返信することができます。
62
34
 
63
35
  ### Push Message
64
36
 
@@ -15,8 +15,9 @@
15
15
  outputs: 1,
16
16
  icon: "comment.png",
17
17
  align: "right",
18
- label: function() {
19
- return this.name||"BloadcastMessage";
18
+ paletteLabel: 'Bloadcast',
19
+ label: function () {
20
+ return this.name || 'BloadcastMessage';
20
21
  }
21
22
  });
22
23
  </script>
@@ -14,8 +14,9 @@
14
14
  outputs: 0,
15
15
  icon: "comment.png",
16
16
  align: "right",
17
+ paletteLabel: 'Push',
17
18
  label: function() {
18
- return this.name||"PushMessage";
19
+ return this.name || 'PushMessage';
19
20
  }
20
21
  });
21
22
  </script>
@@ -14,8 +14,9 @@
14
14
  outputs:0,
15
15
  icon: "comment.png",
16
16
  align: "right",
17
- label: function() {
18
- return this.name||"ReplyMessage";
17
+ paletteLabel: 'Reply',
18
+ label: function () {
19
+ return this.name || 'ReplyMessage';
19
20
  }
20
21
  });
21
22
  </script>
@@ -38,7 +39,7 @@
38
39
 
39
40
  <div class="form-row">
40
41
  <label for="node-input-replyMessage"><i class="icon-tag"></i> ReplyMessage</label>
41
- <input type="text" id="node-input-replyMessage" placeholder="空の場合はおうむ返しします">
42
+ <input type="text" id="node-input-replyMessage" placeholder="static text message here">
42
43
  </div>
43
44
  </script>
44
45
 
@@ -16,13 +16,13 @@ module.exports = (RED) => {
16
16
  };
17
17
  }
18
18
 
19
- if(lineconfig.channelSecret === '' || lineconfig.channelAccessToken === '') {
20
- this.error(RED._("token not found"));
19
+ if (lineconfig.channelSecret === '' || lineconfig.channelAccessToken === '') {
20
+ this.error(RED._('token not found'));
21
21
  }
22
22
 
23
23
  const client = new line.Client(lineconfig);
24
24
 
25
- //メインの処理
25
+ // 旧仕様
26
26
  const handleEvent = (event) => {
27
27
  if (event.type !== 'message') {
28
28
  return Promise.resolve(null);
@@ -31,13 +31,13 @@ module.exports = (RED) => {
31
31
  if (event.message.type === 'text') {
32
32
  return client.replyMessage(event.replyToken, {
33
33
  type: 'text',
34
- text: config.replyMessage || event.message.text //実際に返信の言葉を入れる箇所
35
- });
34
+ text: config.replyMessage || event.message.text // 実際に返信の言葉を入れる箇所
35
+ });
36
36
  } else if (event.message.type === 'flex') {
37
37
  const message_text = event.message.altText;
38
38
 
39
39
  return client.replyMessage(event.replyToken, {
40
- type: "flex",
40
+ type: 'flex',
41
41
  altText: message_text,
42
42
  contents: event.message.text
43
43
  });
@@ -52,27 +52,59 @@ module.exports = (RED) => {
52
52
  }
53
53
  }
54
54
 
55
- node.on('input', (msg) => {
56
- Promise
57
- .all(msg.payload.events.map(handleEvent))
58
- .then(result => {
59
- // [{}]が返ってきてる
60
- if (result.length === 1 && 0 === Object.keys(result[0]).length) {
61
- result = {status: 200, message: 'Success'}
62
- }
63
- msg.payload = result;
64
- node.send(msg)
65
- }).catch(err => {
66
- console.log(err);
67
- node.error(err);
55
+ // 新仕様
56
+ const reply = (msg) => {
57
+ if (!msg.line || !msg.line.event || !msg.line.event.type) {
58
+ throw 'no valid LINE event found within msg';
59
+ } else if (!msg.line.event.replyToken) {
60
+ throw 'no replyable LINE event received';
61
+ } else if (!msg.payload) {
62
+ throw 'reply content (msg.payload) is empty';
63
+ } else if (typeof msg.payload === 'object' && msg.payload.type) {
64
+ // payloadがオブジェクトの場合はメッセージオブジェクト扱いで送信される
65
+ return client.replyMessage(msg.line.event.replyToken, msg.payload);
66
+ } else {
67
+ // payloadがそれ以外なら強制的に文字列に変換しテキストメッセージ扱いで送信する
68
+ return client.replyMessage(msg.line.event.replyToken, {
69
+ type: 'text',
70
+ text: config.replyMessage || String(msg.payload)
68
71
  });
72
+ }
73
+ }
74
+
75
+ node.on('input', async (msg) => {
76
+ if (msg.line && msg.line.event) {
77
+ // 新仕様
78
+ try {
79
+ const result = await reply(msg);
80
+ console.info(result);
81
+ } catch (err) {
82
+ console.warn(err);
83
+ node.error(err);
84
+ }
85
+ } else if (msg.payload.events) {
86
+ // 旧仕様
87
+ Promise
88
+ .all(msg.payload.events.map(handleEvent))
89
+ .then(result => {
90
+ // [{}]が返ってきてる
91
+ if (result.length === 1 && 0 === Object.keys(result[0]).length) {
92
+ result = { status: 200, message: 'Success' }
93
+ }
94
+ msg.payload = result;
95
+ node.send(msg)
96
+ }).catch(err => {
97
+ console.log(err);
98
+ node.error(err);
99
+ });
100
+ }
69
101
  });
70
102
  }
71
103
 
72
- RED.nodes.registerType("ReplyMessage", main, {
104
+ RED.nodes.registerType('ReplyMessage', main, {
73
105
  credentials: {
74
- channelSecret: { type:"password" },
75
- channelAccessToken: { type:"password" },
106
+ channelSecret: { type:'password' },
107
+ channelAccessToken: { type:'password' },
76
108
  }
77
109
  });
78
110
  }
@@ -0,0 +1,59 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('Webhook', {
3
+ category: 'LINE',
4
+ color: '#01B301',
5
+ defaults: {
6
+ name: { value: '' },
7
+ url: { value: '', required: true }
8
+ },
9
+ inputs: 0,
10
+ outputs: 1,
11
+ icon: 'inject.svg',
12
+ paletteLabel: 'Webhook',
13
+ label: function () {
14
+ if (this.name) {
15
+ return this.name;
16
+ } else if (this.url) {
17
+ let root = RED.settings.httpNodeRoot;
18
+ if (root.slice(-1) != '/') {
19
+ root = root + '/';
20
+ }
21
+ if (this.url.charAt(0) == '/') {
22
+ root += this.url.slice(1);
23
+ } else {
24
+ root += this.url;
25
+ }
26
+ return root;
27
+ } else {
28
+ return 'Webhook';
29
+ }
30
+ },
31
+ oneditprepare: function () {
32
+ let root = RED.settings.httpNodeRoot;
33
+ if (root.slice(-1) == '/') {
34
+ root = root.slice(0, -1);
35
+ }
36
+ if (root == '') {
37
+ $('#node-input-tip').hide();
38
+ } else {
39
+ $('#node-input-path').html(root);
40
+ $('#node-input-tip').show();
41
+ }
42
+ }
43
+ });
44
+ </script>
45
+
46
+ <script type="text/x-red" data-template-name="Webhook">
47
+ <div class="form-row">
48
+ <label for="node-input-url"><i class="fa fa-globe"></i> Path</label>
49
+ <input id="node-input-url" type="text" placeholder="/webhook">
50
+ </div>
51
+ <div class="form-row">
52
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
53
+ <input id="node-input-name" type="text" placeholder="Name">
54
+ </div>
55
+ </script>
56
+
57
+ <script type="text/x-red" data-help-name="Webhook">
58
+ <p>A webhook endpoint node that receives from LINE platform when fire any events occurred.</p>
59
+ </script>
@@ -0,0 +1,191 @@
1
+ /**
2
+ * 公式ノード http in ベース
3
+ * @see https://raw.githubusercontent.com/node-red/node-red/master/packages/node_modules/@node-red/nodes/core/network/21-httpin.js
4
+ */
5
+
6
+ module.exports = function (RED) {
7
+ 'use strict';
8
+
9
+ const bodyParser = require('body-parser');
10
+ const cors = require('cors');
11
+
12
+ function createResponseWrapper(node, res) {
13
+ const wrapper = {
14
+ _res: res
15
+ };
16
+ const toWrap = [
17
+ 'append',
18
+ 'attachment',
19
+ 'cookie',
20
+ 'clearCookie',
21
+ 'download',
22
+ 'end',
23
+ 'format',
24
+ 'get',
25
+ 'json',
26
+ 'jsonp',
27
+ 'links',
28
+ 'location',
29
+ 'redirect',
30
+ 'render',
31
+ 'send',
32
+ 'sendfile',
33
+ 'sendFile',
34
+ 'sendStatus',
35
+ 'set',
36
+ 'status',
37
+ 'type',
38
+ 'vary'
39
+ ];
40
+ toWrap.forEach(function (f) {
41
+ wrapper[f] = function () {
42
+ node.warn(RED._('webhook.errors.deprecated-call', { method: 'msg.res.' + f }));
43
+ const result = res[f].apply(res, arguments);
44
+ if (result === res) {
45
+ return wrapper;
46
+ } else {
47
+ return result;
48
+ }
49
+ }
50
+ });
51
+ return wrapper;
52
+ }
53
+
54
+ let corsHandler = function (req, res, next) { next(); }
55
+
56
+ if (RED.settings.httpNodeCors) {
57
+ corsHandler = cors(RED.settings.httpNodeCors);
58
+ RED.httpNode.options('*', corsHandler);
59
+ }
60
+
61
+ // LINEイベント種別から適切な文字列を返す
62
+ function eventToString(event) {
63
+ if (event.type === 'message') {
64
+ // メッセージ系イベント
65
+ switch (event.message.type) {
66
+ case 'text':
67
+ return event.message.text;
68
+ case 'image':
69
+ case 'video':
70
+ case 'audio':
71
+ case 'file':
72
+ // 実装予定
73
+ return '(media)';
74
+ case 'location':
75
+ return event.message.address;
76
+ case 'sticker':
77
+ return '(sticker)';
78
+ default:
79
+ return '(unknown message event)';
80
+ }
81
+ } else {
82
+ // メッセージじゃない
83
+ switch (event.type) {
84
+ case 'unsend':
85
+ return '(unsend)';
86
+ case 'follow':
87
+ return '(follow)';
88
+ case 'unfollow':
89
+ return '(unfollow)';
90
+ case 'join':
91
+ return '(join)';
92
+ case 'leave':
93
+ return '(leave)';
94
+ case 'memberJoined':
95
+ return '(memberJoined)';
96
+ case 'memberLeft':
97
+ return '(memberLeft)';
98
+ case 'postback':
99
+ if (event.postback.params.datetime) return event.postback.params.datetime;
100
+ else if (event.postback.params.newRichMenuAliasId) return event.postback.params.newRichMenuAliasId;
101
+ else '(postback)';
102
+ case 'videoPlayComplete':
103
+ return 'videoPlayComplete';
104
+ case 'beacon':
105
+ return (event.beacon.dm) ? event.beacon.dm : event.beacon.hwid;
106
+ case 'accountLink':
107
+ return '(accountLink)';
108
+ case 'things': // typeによって連携・連携解除・シナリオ実行の3種がある
109
+ return event.things.deviceId;
110
+ default:
111
+ return '(unknown event)';
112
+ }
113
+ }
114
+ }
115
+
116
+ function Webhook(config) {
117
+ RED.nodes.createNode(this, config);
118
+ if (RED.settings.httpNodeRoot !== false) {
119
+
120
+ if (!config.url) {
121
+ this.warn(RED._('webhook.errors.missing-path'));
122
+ return;
123
+ }
124
+ this.url = config.url;
125
+ if (this.url[0] !== '/') {
126
+ this.url = '/' + this.url;
127
+ }
128
+
129
+ const node = this;
130
+
131
+ // エラー時
132
+ this.errorHandler = function (err, req, res, next) {
133
+ node.warn(err);
134
+ res.sendStatus(500);
135
+ };
136
+
137
+ // 受信してmsgを送り出すところ
138
+ this.callback = function (req, res) {
139
+ const msgid = RED.util.generateId();
140
+ res._msgid = msgid;
141
+ // Webhook共通
142
+ if (req.body.destination && req.body.events) {
143
+ // イベントと同数のmsgを送信
144
+ req.body.events.forEach(event => {
145
+ node.send({
146
+ _msgid: msgid,
147
+ req: req,
148
+ res: createResponseWrapper(node, res),
149
+ line: {
150
+ destination: req.body.destination,
151
+ event: event, // 個別のイベント
152
+ events: req.body.events, // 生イベント配列
153
+ },
154
+ payload: eventToString(event)
155
+ });
156
+ });
157
+ }
158
+ // 空データをLINEプラットフォームへ返す (200 OK)
159
+ const wrapper = createResponseWrapper(node, res);
160
+ wrapper._res.set('content-length', 0);
161
+ wrapper._res.status(200).send('');
162
+ };
163
+
164
+ const httpMiddleware = function (req, res, next) { next(); }
165
+
166
+ if (RED.settings.httpNodeMiddleware) {
167
+ if (typeof RED.settings.httpNodeMiddleware === 'function' || Array.isArray(RED.settings.httpNodeMiddleware)) {
168
+ httpMiddleware = RED.settings.httpNodeMiddleware;
169
+ }
170
+ }
171
+
172
+ const maxApiRequestSize = RED.settings.apiMaxLength || '5mb';
173
+ const jsonParser = bodyParser.json({ limit: maxApiRequestSize });
174
+
175
+ // 必ずPOSTで受ける
176
+ RED.httpNode.post(this.url, httpMiddleware, corsHandler, jsonParser, this.callback, this.errorHandler);
177
+
178
+ this.on('close', function () {
179
+ const node = this;
180
+ RED.httpNode._router.stack.forEach(function (route, i, routes) {
181
+ if (route.route && route.route.path === node.url && route.route.methods['post']) {
182
+ routes.splice(i, 1);
183
+ }
184
+ });
185
+ });
186
+ } else {
187
+ this.warn(RED._('webhook.errors.not-created'));
188
+ }
189
+ }
190
+ RED.nodes.registerType('Webhook', Webhook);
191
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-line-messaging-api",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "LINE Messaging APIです",
5
5
  "main": "line.js",
6
6
  "repository": {
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "node-red": {
15
15
  "nodes": {
16
+ "Webhook": "./nodes/webhook/webhook.js",
16
17
  "ReplyMessage": "./nodes/reply/reply.js",
17
18
  "PushMessage": "./nodes/push/push.js",
18
19
  "BloadcastMessage": "./nodes/bloadcast/bloadcast.js",
@@ -28,6 +29,8 @@
28
29
  "license": "Apache 2.0",
29
30
  "dependencies": {
30
31
  "@line/bot-sdk": "^7.2.0",
32
+ "body-parser": "^1.19.1",
33
+ "cors": "^2.8.5",
31
34
  "express": "^4.17.1"
32
35
  }
33
36
  }