UNPKG

30.6 kBPlain TextView Raw
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 */
20import { EventEmitter } from 'events'
21import { instanceToClass } from 'clone-class'
22
23import {
24 MessagePayload,
25 MessageQueryFilter,
26 MessageType,
27} from 'wechaty-puppet'
28
29import { escapeRegExp } from '../helper-functions/pure/escape-regexp'
30import { timestampToDate } from '../helper-functions/pure/timestamp-to-date'
31
32import {
33 Wechaty,
34} from '../wechaty'
35import {
36 AT_SEPARATOR_REGEX,
37 FileBox,
38
39 log,
40 Raven,
41 looseInstanceOfFileBox,
42} from '../config'
43import {
44 Sayable,
45} from '../types'
46
47import {
48 Contact,
49} from './contact'
50import {
51 Room,
52} from './room'
53import {
54 UrlLink,
55} from './url-link'
56import {
57 MiniProgram,
58} from './mini-program'
59import { 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 */
66class 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
1056function 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
1069export {
1070 Message,
1071 wechatifyMessage,
1072}