1 | /**
|
2 | * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
|
3 | *
|
4 | * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
|
5 | * Wechaty Contributors <https://github.com/wechaty>.
|
6 | *
|
7 | * Licensed under the Apache License, Version 2.0 (the "License");
|
8 | * you may not use this file except in compliance with the License.
|
9 | * You may obtain a copy of the License at
|
10 | *
|
11 | * http://www.apache.org/licenses/LICENSE-2.0
|
12 | *
|
13 | * Unless required by applicable law or agreed to in writing, software
|
14 | * distributed under the License is distributed on an "AS IS" BASIS,
|
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16 | * See the License for the specific language governing permissions and
|
17 | * limitations under the License.
|
18 | *
|
19 | */
|
20 | import { EventEmitter } from 'events'
|
21 | import { instanceToClass } from 'clone-class'
|
22 |
|
23 | import {
|
24 | MessagePayload,
|
25 | MessageQueryFilter,
|
26 | MessageType,
|
27 | } from 'wechaty-puppet'
|
28 |
|
29 | import { escapeRegExp } from '../helper-functions/pure/escape-regexp'
|
30 | import { timestampToDate } from '../helper-functions/pure/timestamp-to-date'
|
31 |
|
32 | import {
|
33 | Wechaty,
|
34 | } from '../wechaty'
|
35 | import {
|
36 | AT_SEPARATOR_REGEX,
|
37 | FileBox,
|
38 |
|
39 | log,
|
40 | Raven,
|
41 | looseInstanceOfFileBox,
|
42 | } from '../config'
|
43 | import {
|
44 | Sayable,
|
45 | } from '../types'
|
46 |
|
47 | import {
|
48 | Contact,
|
49 | } from './contact'
|
50 | import {
|
51 | Room,
|
52 | } from './room'
|
53 | import {
|
54 | UrlLink,
|
55 | } from './url-link'
|
56 | import {
|
57 | MiniProgram,
|
58 | } from './mini-program'
|
59 | import { Image } from './image'
|
60 |
|
61 | /**
|
62 | * All wechat messages will be encapsulated as a Message.
|
63 | *
|
64 | * [Examples/Ding-Dong-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts}
|
65 | */
|
66 | class Message extends EventEmitter implements Sayable {
|
67 |
|
68 | static get wechaty (): Wechaty { throw new Error('This class can not be used directly. See: https://github.com/wechaty/wechaty/issues/2027') }
|
69 | get wechaty (): Wechaty { throw new Error('This class can not be used directly. See: https://github.com/wechaty/wechaty/issues/2027') }
|
70 |
|
71 | /**
|
72 | *
|
73 | * Static Properties
|
74 | *
|
75 | */
|
76 |
|
77 | /**
|
78 | * @ignore
|
79 | */
|
80 | public static readonly Type = MessageType
|
81 |
|
82 | /**
|
83 | * Find message in cache
|
84 | */
|
85 | public static async find<T extends typeof Message> (
|
86 | this : T,
|
87 | query : string | MessageQueryFilter,
|
88 | ): Promise<T['prototype'] | null> {
|
89 | log.verbose('Message', 'find(%s)', JSON.stringify(query))
|
90 |
|
91 | if (typeof query === 'string') {
|
92 | query = { text: query }
|
93 | }
|
94 |
|
95 | const messageList = await this.findAll(query)
|
96 | if (messageList.length < 1) {
|
97 | return null
|
98 | }
|
99 |
|
100 | if (messageList.length > 1) {
|
101 | log.warn('Message', 'findAll() got more than one(%d) result', messageList.length)
|
102 | }
|
103 |
|
104 | return messageList[0]!
|
105 | }
|
106 |
|
107 | /**
|
108 | * Find messages in cache
|
109 | */
|
110 | public static async findAll<T extends typeof Message> (
|
111 | this : T,
|
112 | query? : MessageQueryFilter,
|
113 | ): Promise<Array<T['prototype']>> {
|
114 | log.verbose('Message', 'findAll(%s)', JSON.stringify(query) || '')
|
115 |
|
116 | const invalidDict: { [id: string]: true } = {}
|
117 |
|
118 | try {
|
119 | const MessageIdList = await this.wechaty.puppet.messageSearch(query)
|
120 | const messageList = MessageIdList.map(id => this.load(id))
|
121 | await Promise.all(
|
122 | messageList.map(
|
123 | message => message.ready()
|
124 | .catch(e => {
|
125 | log.warn('Room', 'findAll() message.ready() rejection: %s', e)
|
126 | invalidDict[message.id] = true
|
127 | })
|
128 | ),
|
129 | )
|
130 |
|
131 | return messageList.filter(message => !invalidDict[message.id])
|
132 |
|
133 | } catch (e) {
|
134 | log.warn('Message', 'findAll() rejected: %s', e.message)
|
135 | console.error(e)
|
136 | Raven.captureException(e)
|
137 | return [] // fail safe
|
138 | }
|
139 | }
|
140 |
|
141 | /**
|
142 | * Create a Mobile Terminated Message
|
143 | * @ignore
|
144 | * @ignore
|
145 | * "mobile originated" or "mobile terminated"
|
146 | * https://www.tatango.com/resources/video-lessons/video-mo-mt-sms-messaging/
|
147 | */
|
148 | public static load (id: string): Message {
|
149 | log.verbose('Message', 'static load(%s)', id)
|
150 |
|
151 | /**
|
152 | * Must NOT use `Message` at here
|
153 | * MUST use `this` at here
|
154 | *
|
155 | * because the class will be `cloneClass`-ed
|
156 | */
|
157 | const msg = new this(id)
|
158 |
|
159 | return msg
|
160 | }
|
161 |
|
162 | /**
|
163 | *
|
164 | * Instance Properties
|
165 | * @hidden
|
166 | *
|
167 | */
|
168 | protected payload?: MessagePayload
|
169 |
|
170 | /**
|
171 | * @hideconstructor
|
172 | */
|
173 | protected constructor (
|
174 | public readonly id: string,
|
175 | ) {
|
176 | super()
|
177 | log.verbose('Message', 'constructor(%s) for class %s',
|
178 | id || '',
|
179 | this.constructor.name,
|
180 | )
|
181 |
|
182 | const MyClass = instanceToClass(this, Message)
|
183 |
|
184 | if (MyClass === Message) {
|
185 | throw new Error('Message class can not be instantiated directly! See: https://github.com/wechaty/wechaty/issues/1217')
|
186 | }
|
187 |
|
188 | if (!this.wechaty.puppet) {
|
189 | throw new Error('Message class can not be instantiated without a puppet!')
|
190 | }
|
191 | }
|
192 |
|
193 | /**
|
194 | * @ignore
|
195 | */
|
196 | public override toString () {
|
197 | if (!this.payload) {
|
198 | return this.constructor.name
|
199 | }
|
200 |
|
201 | const msgStrList = [
|
202 | 'Message',
|
203 | `#${MessageType[this.type()]}`,
|
204 | '[',
|
205 | '🗣',
|
206 | this.talker(),
|
207 | this.room()
|
208 | ? '@👥' + this.room()
|
209 | : '',
|
210 | ']',
|
211 | ]
|
212 | if (this.type() === Message.Type.Text
|
213 | || this.type() === Message.Type.Unknown
|
214 | ) {
|
215 | msgStrList.push(`\t${this.text().substr(0, 70)}`)
|
216 | } else {
|
217 | log.silly('Message', 'toString() for message type: %s(%s)', Message.Type[this.type()], this.type())
|
218 |
|
219 | if (!this.payload) {
|
220 | throw new Error('no payload')
|
221 | }
|
222 | }
|
223 |
|
224 | return msgStrList.join('')
|
225 | }
|
226 |
|
227 | public conversation (): Contact | Room {
|
228 | if (this.room()) {
|
229 | return this.room()!
|
230 | } else {
|
231 | return this.talker()
|
232 | }
|
233 | }
|
234 |
|
235 | /**
|
236 | * Get the talker of a message.
|
237 | * @returns {Contact}
|
238 | * @example
|
239 | * const bot = new Wechaty()
|
240 | * bot
|
241 | * .on('message', async m => {
|
242 | * const talker = msg.talker()
|
243 | * const text = msg.text()
|
244 | * const room = msg.room()
|
245 | * if (room) {
|
246 | * const topic = await room.topic()
|
247 | * console.log(`Room: ${topic} Contact: ${talker.name()} Text: ${text}`)
|
248 | * } else {
|
249 | * console.log(`Contact: ${talker.name()} Text: ${text}`)
|
250 | * }
|
251 | * })
|
252 | * .start()
|
253 | */
|
254 | public talker (): Contact {
|
255 | if (!this.payload) {
|
256 | throw new Error('no payload')
|
257 | }
|
258 |
|
259 | // if (contact) {
|
260 | // this.payload.from = contact
|
261 | // return
|
262 | // }
|
263 |
|
264 | const talkerId = this.payload.fromId
|
265 | if (!talkerId) {
|
266 | // Huan(202011): It seems that the fromId will never be null?
|
267 | // return null
|
268 | throw new Error('payload.fromId is null?')
|
269 | }
|
270 |
|
271 | const talker = this.wechaty.Contact.load(talkerId)
|
272 | return talker
|
273 | }
|
274 |
|
275 | /**
|
276 | * @depreacated Use `message.talker()` to replace `message.from()`
|
277 | * https://github.com/wechaty/wechaty/issues/2094
|
278 | */
|
279 | public from (): null | Contact {
|
280 | log.warn('Message', 'from() is deprecated, use talker() instead. Call stack: %s',
|
281 | new Error().stack,
|
282 | )
|
283 | try {
|
284 | return this.talker()
|
285 | } catch (e) {
|
286 | return null
|
287 | }
|
288 | }
|
289 |
|
290 | /**
|
291 | * Get the destination of the message
|
292 | * Message.to() will return null if a message is in a room, use Message.room() to get the room.
|
293 | * @returns {(Contact|null)}
|
294 | */
|
295 | public to (): null | Contact {
|
296 | if (!this.payload) {
|
297 | throw new Error('no payload')
|
298 | }
|
299 |
|
300 | const toId = this.payload.toId
|
301 | if (!toId) {
|
302 | return null
|
303 | }
|
304 |
|
305 | const to = this.wechaty.Contact.load(toId)
|
306 | return to
|
307 | }
|
308 |
|
309 | /**
|
310 | * Get the room from the message.
|
311 | * If the message is not in a room, then will return `null`
|
312 | *
|
313 | * @returns {(Room | null)}
|
314 | * @example
|
315 | * const bot = new Wechaty()
|
316 | * bot
|
317 | * .on('message', async m => {
|
318 | * const contact = msg.from()
|
319 | * const text = msg.text()
|
320 | * const room = msg.room()
|
321 | * if (room) {
|
322 | * const topic = await room.topic()
|
323 | * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`)
|
324 | * } else {
|
325 | * console.log(`Contact: ${contact.name()} Text: ${text}`)
|
326 | * }
|
327 | * })
|
328 | * .start()
|
329 | */
|
330 | public room (): null | Room {
|
331 | if (!this.payload) {
|
332 | throw new Error('no payload')
|
333 | }
|
334 | const roomId = this.payload.roomId
|
335 | if (!roomId) {
|
336 | return null
|
337 | }
|
338 |
|
339 | const room = this.wechaty.Room.load(roomId)
|
340 | return room
|
341 | }
|
342 |
|
343 | /**
|
344 | * Get the text content of the message
|
345 | *
|
346 | * @returns {string}
|
347 | * @example
|
348 | * const bot = new Wechaty()
|
349 | * bot
|
350 | * .on('message', async m => {
|
351 | * const contact = msg.from()
|
352 | * const text = msg.text()
|
353 | * const room = msg.room()
|
354 | * if (room) {
|
355 | * const topic = await room.topic()
|
356 | * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`)
|
357 | * } else {
|
358 | * console.log(`Contact: ${contact.name()} Text: ${text}`)
|
359 | * }
|
360 | * })
|
361 | * .start()
|
362 | */
|
363 | public text (): string {
|
364 | if (!this.payload) {
|
365 | throw new Error('no payload')
|
366 | }
|
367 |
|
368 | return this.payload.text || ''
|
369 | }
|
370 |
|
371 | /**
|
372 | * Get the recalled message
|
373 | *
|
374 | * @example
|
375 | * const bot = new Wechaty()
|
376 | * bot
|
377 | * .on('message', async m => {
|
378 | * if (m.type() === MessageType.Recalled) {
|
379 | * const recalledMessage = await m.toRecalled()
|
380 | * console.log(`Message: ${recalledMessage} has been recalled.`)
|
381 | * }
|
382 | * })
|
383 | * .start()
|
384 | */
|
385 | public async toRecalled (): Promise<Message | null> {
|
386 | if (this.type() !== MessageType.Recalled) {
|
387 | throw new Error('Can not call toRecalled() on message which is not recalled type.')
|
388 | }
|
389 | const originalMessageId = this.text()
|
390 | if (!originalMessageId) {
|
391 | throw new Error('Can not find recalled message')
|
392 | }
|
393 | try {
|
394 | const message = this.wechaty.Message.load(originalMessageId)
|
395 | await message.ready()
|
396 | return message
|
397 | } catch (e) {
|
398 | log.verbose(`Can not retrieve the recalled message with id ${originalMessageId}.`)
|
399 | return null
|
400 | }
|
401 | }
|
402 |
|
403 | public say (text: string) : Promise<void | Message>
|
404 | public say (num: number) : Promise<void | Message>
|
405 | public say (message: Message) : Promise<void | Message>
|
406 | public say (contact: Contact) : Promise<void | Message>
|
407 | public say (file: FileBox) : Promise<void | Message>
|
408 | public say (url: UrlLink) : Promise<void | Message>
|
409 | public say (mini: MiniProgram) : Promise<void | Message>
|
410 |
|
411 | // Huan(202006): allow fall down to the definition to get more flexibility.
|
412 | // public say (...args: never[]): Promise<never>
|
413 |
|
414 | /**
|
415 | * Reply a Text or Media File message to the sender.
|
416 | * > Tips:
|
417 | * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
|
418 | *
|
419 | * @see {@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts|Examples/ding-dong-bot}
|
420 | * @param {(string | Contact | FileBox | UrlLink | MiniProgram)} textOrContactOrFile
|
421 | * send text, Contact, or file to bot. </br>
|
422 | * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
|
423 | * @param {(Contact|Contact[])} [mention]
|
424 | * If this is a room message, when you set mention param, you can `@` Contact in the room.
|
425 | * @returns {Promise<void | Message>}
|
426 | *
|
427 | * @example
|
428 | * import { FileBox } from 'wechaty'
|
429 | * const bot = new Wechaty()
|
430 | * bot
|
431 | * .on('message', async m => {
|
432 | *
|
433 | * // 1. send Image
|
434 | *
|
435 | * if (/^ding$/i.test(m.text())) {
|
436 | * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png')
|
437 | * await msg.say(fileBox)
|
438 | * const message = await msg.say(fileBox) // only supported by puppet-padplus
|
439 | * }
|
440 | *
|
441 | * // 2. send Text
|
442 | *
|
443 | * if (/^dong$/i.test(m.text())) {
|
444 | * await msg.say('ding')
|
445 | * const message = await msg.say('ding') // only supported by puppet-padplus
|
446 | * }
|
447 | *
|
448 | * // 3. send Contact
|
449 | *
|
450 | * if (/^lijiarui$/i.test(m.text())) {
|
451 | * const contactCard = await bot.Contact.find({name: 'lijiarui'})
|
452 | * if (!contactCard) {
|
453 | * console.log('not found')
|
454 | * return
|
455 | * }
|
456 | * await msg.say(contactCard)
|
457 | * const message = await msg.say(contactCard) // only supported by puppet-padplus
|
458 | * }
|
459 | *
|
460 | * // 4. send Link
|
461 | *
|
462 | * if (/^link$/i.test(m.text())) {
|
463 | * const linkPayload = new UrlLink ({
|
464 | * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love',
|
465 | * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4',
|
466 | * title : 'Welcome to Wechaty',
|
467 | * url : 'https://github.com/wechaty/wechaty',
|
468 | * })
|
469 | * await msg.say(linkPayload)
|
470 | * const message = await msg.say(linkPayload) // only supported by puppet-padplus
|
471 | * }
|
472 | *
|
473 | * // 5. send MiniProgram
|
474 | *
|
475 | * if (/^link$/i.test(m.text())) {
|
476 | * const miniProgramPayload = new MiniProgram ({
|
477 | * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com
|
478 | * appid : '', //optional, get from mp.weixin.qq.com
|
479 | * title : '', //optional
|
480 | * pagepath : '', //optional
|
481 | * description : '', //optional
|
482 | * thumbnailurl : '', //optional
|
483 | * })
|
484 | * await msg.say(miniProgramPayload)
|
485 | * const message = await msg.say(miniProgramPayload) // only supported by puppet-padplus
|
486 | * }
|
487 | *
|
488 | * })
|
489 | * .start()
|
490 | */
|
491 | public async say (
|
492 | something : string
|
493 | | number
|
494 | | Message
|
495 | | Contact
|
496 | | FileBox
|
497 | | UrlLink
|
498 | | MiniProgram,
|
499 | ): Promise<void | Message> {
|
500 | log.verbose('Message', 'say(%s)', something)
|
501 |
|
502 | // const user = this.wechaty.puppet.userSelf()
|
503 | const talker = this.talker()
|
504 | // const to = this.to()
|
505 | const room = this.room()
|
506 |
|
507 | let conversationId: string
|
508 | let conversation
|
509 |
|
510 | if (room) {
|
511 | conversation = room
|
512 | conversationId = room.id
|
513 | } else if (talker) {
|
514 | conversation = talker
|
515 | conversationId = talker.id
|
516 | } else {
|
517 | throw new Error('neither room nor from?')
|
518 | }
|
519 |
|
520 | /**
|
521 | * Support say a existing message: just forward it.
|
522 | */
|
523 | if (something instanceof Message) {
|
524 | return something.forward(conversation)
|
525 | }
|
526 |
|
527 | // Convert number to string
|
528 | if (typeof something === 'number') {
|
529 | something = String(something)
|
530 | }
|
531 |
|
532 | let msgId: void | string
|
533 | if (typeof something === 'string') {
|
534 | /**
|
535 | * Text Message
|
536 | */
|
537 | let mentionIdList
|
538 | if (talker && await this.mentionSelf()) {
|
539 | mentionIdList = [talker.id]
|
540 | }
|
541 |
|
542 | msgId = await this.wechaty.puppet.messageSendText(
|
543 | conversationId,
|
544 | something,
|
545 | mentionIdList,
|
546 | )
|
547 | } else if (something instanceof Contact) {
|
548 | /**
|
549 | * Contact Card
|
550 | */
|
551 | msgId = await this.wechaty.puppet.messageSendContact(
|
552 | conversationId,
|
553 | something.id,
|
554 | )
|
555 | } else if (looseInstanceOfFileBox(something)) {
|
556 | /**
|
557 | * Be aware of minified codes:
|
558 | * https://stackoverflow.com/questions/1249531/how-to-get-a-javascript-objects-class#comment60309941_1249554
|
559 | */
|
560 |
|
561 | /**
|
562 | * File Message
|
563 | */
|
564 | msgId = await this.wechaty.puppet.messageSendFile(
|
565 | conversationId,
|
566 | something,
|
567 | )
|
568 | } else if (something instanceof UrlLink) {
|
569 | /**
|
570 | * Link Message
|
571 | */
|
572 | msgId = await this.wechaty.puppet.messageSendUrl(
|
573 | conversationId,
|
574 | something.payload,
|
575 | )
|
576 | } else if (something instanceof MiniProgram) {
|
577 | /**
|
578 | * MiniProgram
|
579 | */
|
580 | msgId = await this.wechaty.puppet.messageSendMiniProgram(
|
581 | conversationId,
|
582 | something.payload,
|
583 | )
|
584 | } else {
|
585 | throw new Error('Message.say() received unknown msg: ' + something)
|
586 | }
|
587 | if (msgId) {
|
588 | const msg = this.wechaty.Message.load(msgId)
|
589 | await msg.ready()
|
590 | return msg
|
591 | }
|
592 | }
|
593 |
|
594 | /**
|
595 | * Recall a message.
|
596 | * > Tips:
|
597 | * @returns {Promise<boolean>}
|
598 | *
|
599 | * @example
|
600 | * const bot = new Wechaty()
|
601 | * bot
|
602 | * .on('message', async m => {
|
603 | * const recallMessage = await msg.say('123')
|
604 | * if (recallMessage) {
|
605 | * const isSuccess = await recallMessage.recall()
|
606 | * }
|
607 | * })
|
608 | */
|
609 |
|
610 | public async recall (): Promise<boolean> {
|
611 | log.verbose('Message', 'recall()')
|
612 | const isSuccess = await this.wechaty.puppet.messageRecall(this.id)
|
613 | return isSuccess
|
614 | }
|
615 |
|
616 | /**
|
617 | * Get the type from the message.
|
618 | * > Tips: MessageType is Enum here. </br>
|
619 | * - MessageType.Unknown </br>
|
620 | * - MessageType.Attachment </br>
|
621 | * - MessageType.Audio </br>
|
622 | * - MessageType.Contact </br>
|
623 | * - MessageType.Emoticon </br>
|
624 | * - MessageType.Image </br>
|
625 | * - MessageType.Text </br>
|
626 | * - MessageType.Video </br>
|
627 | * - MessageType.Url </br>
|
628 | * @returns {MessageType}
|
629 | *
|
630 | * @example
|
631 | * const bot = new Wechaty()
|
632 | * if (message.type() === bot.Message.Type.Text) {
|
633 | * console.log('This is a text message')
|
634 | * }
|
635 | */
|
636 | public type (): MessageType {
|
637 | if (!this.payload) {
|
638 | throw new Error('no payload')
|
639 | }
|
640 | return this.payload.type || MessageType.Unknown
|
641 | }
|
642 |
|
643 | /**
|
644 | * Check if a message is sent by self.
|
645 | *
|
646 | * @returns {boolean} - Return `true` for send from self, `false` for send from others.
|
647 | * @example
|
648 | * if (message.self()) {
|
649 | * console.log('this message is sent by myself!')
|
650 | * }
|
651 | */
|
652 | public self (): boolean {
|
653 | const userId = this.wechaty.puppet.selfId()
|
654 | const talker = this.talker()
|
655 |
|
656 | return !!talker && talker.id === userId
|
657 | }
|
658 |
|
659 | /**
|
660 | *
|
661 | * Get message mentioned contactList.
|
662 | *
|
663 | * Message event table as follows
|
664 | *
|
665 | * | | Web | Mac PC Client | iOS Mobile | android Mobile |
|
666 | * | :--- | :--: | :----: | :---: | :---: |
|
667 | * | [You were mentioned] tip ([有人@我]的提示) | ✘ | √ | √ | √ |
|
668 | * | Identify magic code (8197) by copy & paste in mobile | ✘ | √ | √ | ✘ |
|
669 | * | Identify magic code (8197) by programming | ✘ | ✘ | ✘ | ✘ |
|
670 | * | Identify two contacts with the same roomAlias by [You were mentioned] tip | ✘ | ✘ | √ | √ |
|
671 | *
|
672 | * @returns {Promise<Contact[]>} - Return message mentioned contactList
|
673 | *
|
674 | * @example
|
675 | * const contactList = await message.mentionList()
|
676 | * console.log(contactList)
|
677 | */
|
678 | public async mentionList (): Promise<Contact[]> {
|
679 | log.verbose('Message', 'mentionList()')
|
680 |
|
681 | const room = this.room()
|
682 | if (this.type() !== MessageType.Text || !room) {
|
683 | return []
|
684 | }
|
685 |
|
686 | /**
|
687 | * Use mention list if mention list is available
|
688 | * otherwise, process the message and get the mention list
|
689 | */
|
690 | if (this.payload && 'mentionIdList' in this.payload) {
|
691 | const idToContact = async (id: string) => {
|
692 | const contact = this.wechaty.Contact.load(id)
|
693 | await contact.ready()
|
694 | return contact
|
695 | }
|
696 | return Promise.all(this.payload.mentionIdList?.map(idToContact) ?? [])
|
697 | }
|
698 |
|
699 | /**
|
700 | * define magic code `8197` to identify @xxx
|
701 | * const AT_SEPARATOR = String.fromCharCode(8197)
|
702 | */
|
703 | const atList = this.text().split(AT_SEPARATOR_REGEX)
|
704 | // console.log('atList: ', atList)
|
705 | if (atList.length === 0) return []
|
706 |
|
707 | // Using `filter(e => e.indexOf('@') > -1)` to filter the string without `@`
|
708 | const rawMentionList = atList
|
709 | .filter(str => str.includes('@'))
|
710 | .map(str => multipleAt(str))
|
711 |
|
712 | // convert 'hello@a@b@c' to [ 'c', 'b@c', 'a@b@c' ]
|
713 | function multipleAt (str: string) {
|
714 | str = str.replace(/^.*?@/, '@')
|
715 | let name = ''
|
716 | const nameList: string[] = []
|
717 | str.split('@')
|
718 | .filter(mentionName => !!mentionName)
|
719 | .reverse()
|
720 | .forEach(mentionName => {
|
721 | // console.log('mentionName: ', mentionName)
|
722 | name = mentionName + '@' + name
|
723 | nameList.push(name.slice(0, -1)) // get rid of the `@` at beginning
|
724 | })
|
725 | return nameList
|
726 | }
|
727 |
|
728 | let mentionNameList: string[] = []
|
729 | // Flatten Array
|
730 | // see http://stackoverflow.com/a/10865042/1123955
|
731 | mentionNameList = mentionNameList.concat.apply([], rawMentionList)
|
732 | // filter blank string
|
733 | mentionNameList = mentionNameList.filter(s => !!s)
|
734 |
|
735 | log.verbose('Message', 'mentionList() text = "%s", mentionNameList = "%s"',
|
736 | this.text(),
|
737 | JSON.stringify(mentionNameList),
|
738 | )
|
739 |
|
740 | const contactListNested = await Promise.all(
|
741 | mentionNameList.map(
|
742 | name => room.memberAll(name),
|
743 | ),
|
744 | )
|
745 |
|
746 | let contactList: Contact[] = []
|
747 | contactList = contactList.concat.apply([], contactListNested)
|
748 |
|
749 | if (contactList.length === 0) {
|
750 | log.silly('Message', `message.mentionList() can not found member using room.member() from mentionList, mention string: ${JSON.stringify(mentionNameList)}`)
|
751 | }
|
752 | return contactList
|
753 | }
|
754 |
|
755 | /**
|
756 | * @deprecated mention() DEPRECATED. use mentionList() instead.
|
757 | */
|
758 | public async mention (): Promise<Contact[]> {
|
759 | log.warn('Message', 'mention() DEPRECATED. use mentionList() instead. Call stack: %s',
|
760 | new Error().stack,
|
761 | )
|
762 | return this.mentionList()
|
763 | }
|
764 |
|
765 | public async mentionText (): Promise<string> {
|
766 | const text = this.text()
|
767 | const room = this.room()
|
768 |
|
769 | const mentionList = await this.mentionList()
|
770 |
|
771 | if (!room || !mentionList || mentionList.length === 0) {
|
772 | return text
|
773 | }
|
774 |
|
775 | const toAliasName = async (member: Contact) => {
|
776 | const alias = await room.alias(member)
|
777 | const name = member.name()
|
778 | return alias || name
|
779 | }
|
780 |
|
781 | const mentionNameList = await Promise.all(mentionList.map(toAliasName))
|
782 |
|
783 | const textWithoutMention = mentionNameList.reduce((prev, cur) => {
|
784 | const escapedCur = escapeRegExp(cur)
|
785 | const regex = new RegExp(`@${escapedCur}(\u2005|\u0020|$)`)
|
786 | return prev.replace(regex, '')
|
787 | }, text)
|
788 |
|
789 | return textWithoutMention.trim()
|
790 | }
|
791 |
|
792 | /**
|
793 | * Check if a message is mention self.
|
794 | *
|
795 | * @returns {Promise<boolean>} - Return `true` for mention me.
|
796 | * @example
|
797 | * if (await message.mentionSelf()) {
|
798 | * console.log('this message were mentioned me! [You were mentioned] tip ([有人@我]的提示)')
|
799 | * }
|
800 | */
|
801 | public async mentionSelf (): Promise<boolean> {
|
802 | const selfId = this.wechaty.puppet.selfId()
|
803 | const mentionList = await this.mentionList()
|
804 | return mentionList.some(contact => contact.id === selfId)
|
805 | }
|
806 |
|
807 | /**
|
808 | * @ignore
|
809 | */
|
810 | public isReady (): boolean {
|
811 | return !!this.payload
|
812 | }
|
813 |
|
814 | /**
|
815 | * @ignore
|
816 | */
|
817 | public async ready (): Promise<void> {
|
818 | log.verbose('Message', 'ready()')
|
819 |
|
820 | if (this.isReady()) {
|
821 | return
|
822 | }
|
823 |
|
824 | this.payload = await this.wechaty.puppet.messagePayload(this.id)
|
825 |
|
826 | if (!this.payload) {
|
827 | throw new Error('no payload')
|
828 | }
|
829 |
|
830 | const fromId = this.payload.fromId
|
831 | const roomId = this.payload.roomId
|
832 | const toId = this.payload.toId
|
833 |
|
834 | if (roomId) {
|
835 | await this.wechaty.Room.load(roomId).ready()
|
836 | }
|
837 | if (fromId) {
|
838 | await this.wechaty.Contact.load(fromId).ready()
|
839 | }
|
840 | if (toId) {
|
841 | await this.wechaty.Contact.load(toId).ready()
|
842 | }
|
843 | }
|
844 |
|
845 | // case WebMsgType.APP:
|
846 | // if (!this.rawObj) {
|
847 | // throw new Error('no rawObj')
|
848 | // }
|
849 | // switch (this.typeApp()) {
|
850 | // case WebAppMsgType.ATTACH:
|
851 | // if (!this.rawObj.MMAppMsgDownloadUrl) {
|
852 | // throw new Error('no MMAppMsgDownloadUrl')
|
853 | // }
|
854 | // // had set in Message
|
855 | // // url = this.rawObj.MMAppMsgDownloadUrl
|
856 | // break
|
857 |
|
858 | // case WebAppMsgType.URL:
|
859 | // case WebAppMsgType.READER_TYPE:
|
860 | // if (!this.rawObj.Url) {
|
861 | // throw new Error('no Url')
|
862 | // }
|
863 | // // had set in Message
|
864 | // // url = this.rawObj.Url
|
865 | // break
|
866 |
|
867 | // default:
|
868 | // const e = new Error('ready() unsupported typeApp(): ' + this.typeApp())
|
869 | // log.warn('PuppeteerMessage', e.message)
|
870 | // throw e
|
871 | // }
|
872 | // break
|
873 |
|
874 | // case WebMsgType.TEXT:
|
875 | // if (this.typeSub() === WebMsgType.LOCATION) {
|
876 | // url = await puppet.bridge.getMsgPublicLinkImg(this.id)
|
877 | // }
|
878 | // break
|
879 |
|
880 | /**
|
881 | * Forward the received message.
|
882 | *
|
883 | * @param {(Sayable | Sayable[])} to Room or Contact
|
884 | * The recipient of the message, the room, or the contact
|
885 | * @returns {Promise<void>}
|
886 | * @example
|
887 | * const bot = new Wechaty()
|
888 | * bot
|
889 | * .on('message', async m => {
|
890 | * const room = await bot.Room.find({topic: 'wechaty'})
|
891 | * if (room) {
|
892 | * await m.forward(room)
|
893 | * console.log('forward this message to wechaty room!')
|
894 | * }
|
895 | * })
|
896 | * .start()
|
897 | */
|
898 | public async forward (to: Room | Contact): Promise<void | Message> {
|
899 | log.verbose('Message', 'forward(%s)', to)
|
900 |
|
901 | // let roomId
|
902 | // let contactId
|
903 |
|
904 | try {
|
905 | const msgId = await this.wechaty.puppet.messageForward(
|
906 | to.id,
|
907 | this.id,
|
908 | )
|
909 | if (msgId) {
|
910 | const msg = this.wechaty.Message.load(msgId)
|
911 | await msg.ready()
|
912 | return msg
|
913 | }
|
914 | } catch (e) {
|
915 | log.error('Message', 'forward(%s) exception: %s', to, e)
|
916 | throw e
|
917 | }
|
918 | }
|
919 |
|
920 | /**
|
921 | * Message sent date
|
922 | */
|
923 | public date (): Date {
|
924 | if (!this.payload) {
|
925 | throw new Error('no payload')
|
926 | }
|
927 |
|
928 | const timestamp = this.payload.timestamp
|
929 | return timestampToDate(timestamp)
|
930 | }
|
931 |
|
932 | /**
|
933 | * Returns the message age in seconds. <br>
|
934 | *
|
935 | * For example, the message is sent at time `8:43:01`,
|
936 | * and when we received it in Wechaty, the time is `8:43:15`,
|
937 | * then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)`
|
938 | *
|
939 | * @returns {number} message age in seconds.
|
940 | */
|
941 | public age (): number {
|
942 | const ageMilliseconds = Date.now() - this.date().getTime()
|
943 | const ageSeconds = Math.floor(ageMilliseconds / 1000)
|
944 | return ageSeconds
|
945 | }
|
946 |
|
947 | /**
|
948 | * Extract the Media File from the Message, and put it into the FileBox.
|
949 | * > Tips:
|
950 | * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
|
951 | *
|
952 | * @returns {Promise<FileBox>}
|
953 | *
|
954 | * @example <caption>Save media file from a message</caption>
|
955 | * const fileBox = await message.toFileBox()
|
956 | * const fileName = fileBox.name
|
957 | * fileBox.toFile(fileName)
|
958 | */
|
959 | public async toFileBox (): Promise<FileBox> {
|
960 | log.verbose('Message', 'toFileBox()')
|
961 | if (this.type() === Message.Type.Text) {
|
962 | throw new Error('text message no file')
|
963 | }
|
964 | const fileBox = await this.wechaty.puppet.messageFile(this.id)
|
965 | return fileBox
|
966 | }
|
967 |
|
968 | /**
|
969 | * Extract the Image File from the Message, so that we can use different image sizes.
|
970 | * > Tips:
|
971 | * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
|
972 | *
|
973 | * @returns {Image}
|
974 | *
|
975 | * @example <caption>Save image file from a message</caption>
|
976 | * const image = message.toImage()
|
977 | * const fileBox = await image.artwork()
|
978 | * const fileName = fileBox.name
|
979 | * fileBox.toFile(fileName)
|
980 | */
|
981 | public toImage (): Image {
|
982 | log.verbose('Message', 'toImage() for message id: %s', this.id)
|
983 | if (this.type() !== Message.Type.Image) {
|
984 | throw new Error(`not a image type message. type: ${this.type()}`)
|
985 | }
|
986 | return this.wechaty.Image.create(this.id)
|
987 | }
|
988 |
|
989 | /**
|
990 | * Get Share Card of the Message
|
991 | * Extract the Contact Card from the Message, and encapsulate it into Contact class
|
992 | * > Tips:
|
993 | * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
|
994 | * @returns {Promise<Contact>}
|
995 | */
|
996 | public async toContact (): Promise<Contact> {
|
997 | log.verbose('Message', 'toContact()')
|
998 |
|
999 | if (this.type() !== Message.Type.Contact) {
|
1000 | throw new Error('message not a ShareCard')
|
1001 | }
|
1002 |
|
1003 | const contactId = await this.wechaty.puppet.messageContact(this.id)
|
1004 |
|
1005 | if (!contactId) {
|
1006 | throw new Error(`can not get Contact id by message: ${contactId}`)
|
1007 | }
|
1008 |
|
1009 | const contact = this.wechaty.Contact.load(contactId)
|
1010 | await contact.ready()
|
1011 | return contact
|
1012 | }
|
1013 |
|
1014 | public async toUrlLink (): Promise<UrlLink> {
|
1015 | log.verbose('Message', 'toUrlLink()')
|
1016 |
|
1017 | if (!this.payload) {
|
1018 | throw new Error('no payload')
|
1019 | }
|
1020 |
|
1021 | if (this.type() !== Message.Type.Url) {
|
1022 | throw new Error('message not a Url Link')
|
1023 | }
|
1024 |
|
1025 | const urlPayload = await this.wechaty.puppet.messageUrl(this.id)
|
1026 |
|
1027 | if (!urlPayload) {
|
1028 | throw new Error(`no url payload for message ${this.id}`)
|
1029 | }
|
1030 |
|
1031 | return new UrlLink(urlPayload)
|
1032 | }
|
1033 |
|
1034 | public async toMiniProgram (): Promise<MiniProgram> {
|
1035 | log.verbose('Message', 'toMiniProgram()')
|
1036 |
|
1037 | if (!this.payload) {
|
1038 | throw new Error('no payload')
|
1039 | }
|
1040 |
|
1041 | if (this.type() !== Message.Type.MiniProgram) {
|
1042 | throw new Error('message not a MiniProgram')
|
1043 | }
|
1044 |
|
1045 | const miniProgramPayload = await this.wechaty.puppet.messageMiniProgram(this.id)
|
1046 |
|
1047 | if (!miniProgramPayload) {
|
1048 | throw new Error(`no miniProgram payload for message ${this.id}`)
|
1049 | }
|
1050 |
|
1051 | return new MiniProgram(miniProgramPayload)
|
1052 | }
|
1053 |
|
1054 | }
|
1055 |
|
1056 | function wechatifyMessage (wechaty: Wechaty): typeof Message {
|
1057 |
|
1058 | class WechatifiedMessage extends Message {
|
1059 |
|
1060 | static override get wechaty () { return wechaty }
|
1061 | override get wechaty () { return wechaty }
|
1062 |
|
1063 | }
|
1064 |
|
1065 | return WechatifiedMessage
|
1066 |
|
1067 | }
|
1068 |
|
1069 | export {
|
1070 | Message,
|
1071 | wechatifyMessage,
|
1072 | }
|