1 | import EventEmitter from 'events';
|
2 | import bodyParser from 'body-parser';
|
3 | import {Router} from 'express';
|
4 | import Elements from './Elements.js';
|
5 | import Buttons from './Buttons.js';
|
6 | import fetch from './libs/fetch.js';
|
7 | import _ from 'lodash';
|
8 |
|
9 | export {Elements, Buttons};
|
10 |
|
11 | const userCache = {};
|
12 |
|
13 | export async function wait(time) {
|
14 | return new Promise(resolve => setTimeout(() => resolve(), time));
|
15 | }
|
16 |
|
17 | class Bot extends EventEmitter {
|
18 | static Buttons = Buttons;
|
19 | static Elements = Elements;
|
20 |
|
21 | static wait = wait;
|
22 |
|
23 | constructor(token, verification, debug = false) {
|
24 | super();
|
25 |
|
26 | this._token = token;
|
27 | this._debug = debug;
|
28 | this._verification = verification;
|
29 | }
|
30 |
|
31 | async send(to, message) {
|
32 | if (this._debug) {
|
33 | console.log({recipient: {id: to}, message: message ? message.toJSON() : message});
|
34 | }
|
35 |
|
36 | try {
|
37 | await fetch('https://graph.facebook.com/v2.6/me/messages', {
|
38 | method: 'post',
|
39 | query: {access_token: this._token},
|
40 | body: {recipient: {id: to}, message}
|
41 | });
|
42 | } catch (e) {
|
43 | if (e.text) {
|
44 | let text = e.text;
|
45 | try {
|
46 | const err = JSON.parse(e.text).error;
|
47 | text = `${err.type || 'Unknown'}: ${err.message || 'No message'}`;
|
48 | } catch (ee) {
|
49 |
|
50 | }
|
51 |
|
52 | throw Error(text);
|
53 | } else {
|
54 | throw e;
|
55 | }
|
56 | }
|
57 | }
|
58 |
|
59 | async fetchUser(id, fields = 'first_name,last_name,profile_pic', cache = false) {
|
60 | const key = id + fields;
|
61 | let props;
|
62 |
|
63 | if (cache && userCache[key]) {
|
64 | props = userCache[key];
|
65 | props.fromCache = true;
|
66 | } else {
|
67 | const {body} = await fetch(`https://graph.facebook.com/v2.6/${id}`, {
|
68 | query: {access_token: this._token, fields}, json: true
|
69 | });
|
70 |
|
71 | props = body;
|
72 | props.fromCache = false;
|
73 |
|
74 | if (cache) {
|
75 | userCache[key] = props;
|
76 | }
|
77 | }
|
78 |
|
79 | return props;
|
80 | }
|
81 |
|
82 | async handleMessage(input) {
|
83 | const body = JSON.parse(JSON.stringify(input));
|
84 | const message = body.entry[0].messaging[0];
|
85 | Object.assign(message, message.message);
|
86 | delete message.message;
|
87 |
|
88 | message.raw = input;
|
89 |
|
90 | message.sender.fetch = async (fields, cache) => {
|
91 | const props = await this.fetchUser(message.sender.id, fields, cache);
|
92 | Object.assign(message.sender, props);
|
93 | return message.sender;
|
94 | };
|
95 |
|
96 | if (message.postback) {
|
97 | let postback = {};
|
98 |
|
99 | try {
|
100 | postback = JSON.parse(message.postback.payload);
|
101 | } catch (e) {
|
102 |
|
103 | }
|
104 |
|
105 | if (postback.hasOwnProperty('data')) {
|
106 | message.postback = postback;
|
107 | message.data = postback.data;
|
108 | message.event = postback.event;
|
109 |
|
110 | this.emit('postback', message.event, message, message.data);
|
111 |
|
112 | if (postback.hasOwnProperty('event')) {
|
113 | this.emit(message.event, message, message.data);
|
114 | }
|
115 | } else {
|
116 | this.emit('invalid-postback', message, message.postback);
|
117 | }
|
118 |
|
119 | return;
|
120 | }
|
121 |
|
122 | if (message.delivery) {
|
123 | Object.assign(message, message.delivery);
|
124 | message.delivered = message.delivery.mids;
|
125 |
|
126 | delete message.delivery;
|
127 |
|
128 | this.emit('delivery', message, message.delivered);
|
129 | return;
|
130 | }
|
131 |
|
132 | if (message.optin) {
|
133 | message.param = message.optin.ref || true;
|
134 | message.optin = message.param;
|
135 | this.emit('optin', message, message.optin);
|
136 | return;
|
137 | }
|
138 |
|
139 | const attachments = _.groupBy(message.attachments, 'type');
|
140 |
|
141 | if (attachments.image) {
|
142 | message.images = attachments.image.map(a => a.payload.url);
|
143 | }
|
144 |
|
145 | if (attachments.video) {
|
146 | message.videos = attachments.video.map(a => a.payload.url);
|
147 | }
|
148 |
|
149 | if (attachments.audio) {
|
150 | message.audio = attachments.audio.map(a => a.payload.url)[0];
|
151 | }
|
152 |
|
153 | if (attachments.location) {
|
154 | const location = attachments.location[0];
|
155 | message.location = {...location, ...location.payload.coordinates};
|
156 | delete message.location.payload;
|
157 | }
|
158 |
|
159 | message.object = body.object;
|
160 |
|
161 | delete message.attachments;
|
162 |
|
163 | this.emit('message', message);
|
164 | }
|
165 |
|
166 | router() {
|
167 | const router = new Router();
|
168 |
|
169 | router.use(bodyParser.json());
|
170 |
|
171 | router.get('/', (req, res) => {
|
172 | if (req.query['hub.verify_token'] === this._verification) {
|
173 | res.send(req.query['hub.challenge']);
|
174 | } else {
|
175 | res.send('Error, wrong validation token');
|
176 | }
|
177 | });
|
178 |
|
179 | router.post('/', (req, res) => {
|
180 | this.handleMessage(req.body);
|
181 | res.send().status(200);
|
182 | });
|
183 |
|
184 | return router;
|
185 | }
|
186 | }
|
187 |
|
188 | export {Bot};
|
189 |
|
190 | export default Bot;
|