Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Wechaty Could Support Quote Message #2387

Open
hcfw007 opened this issue Mar 18, 2022 · 38 comments
Open

Feature Request: Wechaty Could Support Quote Message #2387

hcfw007 opened this issue Mar 18, 2022 · 38 comments
Assignees

Comments

@hcfw007
Copy link
Member

hcfw007 commented Mar 18, 2022

A lot IMs has quote messages or something like that, e.g. QQ, wechat, wecom, whatsapp and meta messenger, even github! So it would be great for Wechaty to support that.
My early thoughts about this is to add a quote field in message payload which contains the id of the quoted message. The quoted message could be an image or video so save id as reference is good.

  // get quote from a message

  quote (): MessageInterface | null {
    if (!this.payload) {
      throw new Error('no payload')
    }

    let quoteMessageId = this.payload.quoteId
    if (!quoteMessageId) {
      return null // no quote is acceptable
    }

    }

    const quoteMessage = (this.wechaty.Message as typeof MessageImpl).load(quoteMessageId)

    return quoteMessage
  }

For sending a quote message, I'm not sure how to implement it. I'm considering two options:

  1. add some options to say(), like contact.say(content: Sayable, options?: messageOptions)
  2. add a new method, like message.quote(contactOrRoom: Contact | Room, quotedMessage: Message)

Please tell me if you like this feature, and how you want to implement it. Cheers!

@huan
Copy link
Member

huan commented Mar 18, 2022

Thank you very much for the proposal of this great feature, and I agree with you that it is a feature that can be generalized, and should be supported by Wechaty API.

  1. Your quote function design looks good to me. There's one change that need to be done: use undefined to replace the null because we decide not different the null and undefined any more: We are trying to always using undefined when there's a concept like "not exist" or "unknown of existing"
  2. for the say() API, I like the first option that you proposed: say(sayable, sayOptions).

So basically I think your design is OK and please feel free to create a pull request to implement it, and thank you again for helping the Wechaty to improve the API by adding features to it!

@su-chang
Copy link
Member

2. for the say() API, I like the first option that you proposed: say(sayable, sayOptions).

Agree with it.

@hcfw007
Copy link
Member Author

hcfw007 commented Mar 23, 2022

Puppet, wechaty, puppet-service & wechaty-grpc, I think this is the order of projects that requires updates for this feature.

OK I think I have some answers for myself. Please check the draft later.

Update: The draft is ready to be viewed. The test will fail since it relies on the pupet PR.

@huan Please review the puppet PR first, then the wechaty PR, and tell me your thoughts. Cheers.

Draft: #2390

CC: @su-chang @windmemory

@huan
Copy link
Member

huan commented Mar 24, 2022

Thank you very much for creating the pull requests with the draft implementation to the quote feature! I think we should link all PRs in this original issue so that we can have an easy to view index:

After reviewing & thinking about them carefully, I believe it has been turned out that this feature should be implemented by the new Post API:

Post v.s. Message API data structure

In the past, we only have Message class to encapsulate all the simple messages like text, video, mini-program, etc. They are all simple, which means does not support any tree structures, like reply, quote, parent, tap.

That's why we designed the new Post Wechaty User Module (WUM). the design of the Post WUM is for the complex data structure with tree support, which will include the reply, quote, parent, tap, so that it can be used for the Moments, Channel(视频号), Tweet, and Weibo, etc.

Using post.reply()

The semantic meaning of the proposed new API design message.say(sayable, options,) with the reply option, is the same as the Post.reply(), because as we have compared the data structure between the Message and Post:

  1. Message is the simple data structure, without any tree structures
  2. Post is the complex data structure, with the tree support like parent, root, and tap, etc.

And because of that, we will also be able to save lots of code with the native reply support of the post, and reduce the new complexity added to our system.

Design with Post API

To implement the quote() and reply() for quoted messages, here's my new propose with the Post WUM API:

1. Message.quote()

wechaty.on('message', msg => {
  // a message with quote
  const post = await msg.toPost()
  const parent = await post.parent()
  /**
   * You can convert an async iterator to array by:
   *  const sayableList = [... await parent[Symbol.asyncIterator]()]
   *
   * However, the below syntax will be preferred:
   */
  for await (const sayable of parent) {
    console.info('quoted sayable:', sayable) // <- Here we can argue that what kind of return type is better: a `Sayable` or `Message`
  }
})

Related code:

async * [Symbol.asyncIterator] (): AsyncIterableIterator<Sayable> {
log.verbose('Post', '[Symbol.asyncIterator]()')
if (PUPPET.payloads.isPostServer(this.payload)) {
for (const messageId of this.payload.sayableList) {
const message = await this.wechaty.Message.find({ id: messageId })
if (message) {
const sayable = await message.toSayable()
if (sayable) {
yield sayable
}
}
}

2. Post.reply()

wechaty.on('message', msg => {
  const post = await wechaty.Post.builder()
    .add('this is the reply to a quoted messsage')
    .build()

  post.reply(msg) // I'm thinking about to rename the `post.reply()` to `post.replyTo()` so that it will be less confusing.
  await wechaty.publish(post)
})

Related discussion:

To-be improved

I found there lacks some support from the current code base of the Post as well as other improvements, below are what I have already figured out, and there might be more:

  1. The current design of the Post WUM do not contain any talkerId, listenerId, roomId information, I think there has a potential solution by adding a toMessage() so that we can convert a Post to Message so that we can get all the information for that message.
  2. The current Post design does not support call reply() with a Message instance. This will be easily fixed by allowing passing a Message to it because it just needs the messageId.
  3. Consider to rename the post.reply() to post.replyTo()
  4. Improve the return data type support for the async iterator of the Post

Conclusion

By using the new Post WUM implementation with the quoted message feature request, we will use the Message for simple data structure, and use the Post with the complex tree data structure.

And the Post API will significantly reduce the Puppet API code complexity, which will be a huge gain compared with the current PR implementation (The current version of PR contains lots of modifications with our current API and add the same options here and there)

Please let me know what you think @hcfw007 , thank you very much.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 6, 2022

Sorry for replying late. I have given this topic some thoughts.
For my opinion, at this moment, we should just use Message to implement quote messages. Maybe we can use post in the next major version, i'm not sure.
Here are my reasons:

  1. Using a message class to encapsulate a message in an IM is intuitive. All other messages are Message instances, and this should be no different. If we want to abstract items in IM into different classes, that would be a major refraction, and should be carried out in wechaty 2.x.
  2. Quoted message is not a real tree. In a tree, the connection between parent nodes and child nodes should be bi-directional. However in WhatsApp, Wecom and Wechat, we cannot get a quoted list from the original message. Thus we cannot get the child nodes from the parents.
  3. Unlike moment, when you reply to a comment of a moment, you are talking in the scope of the moment. The new reply is also a comment to the moment. However if you quote a quoted message, you are not quoting the original message (the quoted message of the quoted message).

So I believe so far we should continue using this message design and maybe some other day we can move to post.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 7, 2022

@huan please check this out so that we can continue working on this subject.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 12, 2022

@huan ping

@huan
Copy link
Member

huan commented Apr 12, 2022

Here are my answers:

For my opinion, at this moment, we should just use Message to implement quote messages. Maybe we can use post in the next major version, i'm not sure.

Start supporting Post will not break anything. So let's just add Post support in v1.0

All other messages are Message instances, and this should be no different.

That's not true. FileBox, UrlLink, and MiniProgram etc are all different classes instances.

So besides the message.toFileBox() and message.toUrlLink(), of course we can add another message.toPost()

Quoted message is not a real tree. In a tree, the connection between parent nodes and child nodes should be bi-directional.

I don't think a tree should be bi-directional.

According to Wikipedia: In graph theory, a tree is an undirected graph.

However if you quote a quoted message, you are not quoting the original message (the quoted message of the quoted message).

The parentId is for representing/referencing the quoted message. Set it to whatever id you want to, then I believe there will be no issues.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 13, 2022

@huan Thank you for your explanation, however I still have some questions, could you please explain further more.

  1. (I might be wrong with this point, please point it out) In file message or image message, I don't recognize to methods as converters. Considering the following code:
wechaty.on('message', msg => {
  if (message.type() === puppet.types.Message.MiniProgram) {
    const miniProgram = message.toMiniProgram()
  }
})

The miniProgram instance represents the miniProgram object of the message, instead of the miniProgram form of the message. We cannot convert it back to message. It's getting the instance of the miniProgram, instead of converting the message into a miniProgram. (or we can say it's like taking the cake from the box)
With that in mind, I think we should use a different name for toImage, toFileBox(like getImage or getFile maybe?) and other to methods (because they are not converting). toPost is good here.

  1. Why don't we make Post class sayable. In your example, we'll use wechaty.publish(post) to reply a quoted message. This is weird since all conversation messages are delivered by say so far. Or we can use conversation.say(post.toMessage())?
wechaty.on('message', msg => {
  const post = await wechaty.Post.builder()
    .add('this is the reply to a quoted messsage')
    .build()

  post.setReply(msg) // my preference for naming this method:  setReply > replyTo > reply. Because this is only setting the property, not executing the reply process
  await message.say(post.toMessage())
})

@huan
Copy link
Member

huan commented Apr 14, 2022

Here are answers to your new questions:

The miniProgram instance represents the miniProgram object of the message, instead of the miniProgram form of the message.

Those two concepts are the same things: "miniProgram of the message" and "miniProgram from the message"

You can just focus on the miniProgram instance and work with it, that is what it is designed for.

it's getting the instance of the miniProgram, instead of converting the message into a miniProgram. (or we can say it's like taking the cake from the box)

Those two concepts are the same things: "getting" and "converting to" in this context.

We cannot convert it back to message

In Wechaty, you can never convert anything to a message.

Except that you can say() it first, then listen to the wechaty.on('message', ...) event to get it back. (hopefully)

toPost is good here.

We have implemented it here:

public async toPost (): Promise<PostInterface> {
log.verbose('Message', 'toPost()')
if (!this.payload) {
throw new Error('no payload')
}
if (this.type() !== PUPPET.types.Message.Post) {
throw new Error('message type not a Post')
}
const post = PostImpl.load(this.id)
await post.ready()
return post
}

Why don't we make Post class sayable.

Post is already a Sayable.

Please read the code first:

type Sayable =
| ContactInterface
| DelayInterface
| FileBoxInterface
| LocationInterface
| MessageInterface
| MiniProgramInterface
| number
| PostInterface
| string
| UrlLinkInterface


I hope my answers helps. Please feel free to let me know if you have more questions.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 14, 2022

I see in your example code, we have a new message type: Post.
Suppose we have a quote message, the content of the new message is an image, what is the type of the message? And how do we get the image?
(a quoted message cannot be image message in wecom, but it can be in lark)
image

@huan
Copy link
Member

huan commented Apr 14, 2022

I see in your example code, we have a new message type: Post.

Yeah, we have already supported Post as a sayable from the latest version in the main branch.

Suppose we have a quote message, the content of the new message is an image, what is the type of the message? And how do we get the image?

From your above description, my understanding is that there has a text message "123", then another message reply it with an image, correct?

If so, in this case, the code will be like the following:

// Reply to each text message with an image
wechaty.on('message', message => {
  if (message.type() !== MessageType.Text) {
    return
  }

 const post = await wechaty.Post.builder()
    .add(FileBox.fromUrl('https://user-images.githubusercontent.com/13669999/163317404-d86ca9a3-64dd-45c8-a9a5-47d76a368ceb.png'))
    .build()

  post.reply(message) // I'm thinking about to rename the `post.reply()` to `post.replyTo()` so that it will be less confusing.
  await message.say(post)
})

That's it!

Please let me know if this design works for you or not.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 14, 2022

From your above description, my understanding is that there has a text message "123", then another message reply it with an image, correct?

I was asking how do we get the image from the quoting message;
e.g.

wechaty.on('message', message => {
  // message is the quoting message, aka the image message
  if (message.type() !== MessageType.Post) {
    return
  }
  const post = message.toPost()
  for await (const ele of post) {
    const image = ele
    // ...
  }
})

@huan
Copy link
Member

huan commented Apr 14, 2022

I was asking how do we get the image from the quoting message;

Thanks for clarifying your question! The code makes it clear and it's a great question!

How to get Images from Post

wechaty.on('message', message => {
  if (message.type() !== MessageType.Post) {
    return
  }

  const post = message.toPost()
  for await (const sayable of post) {
    if (!FileBox.valid(sayable)) {
      continue
    }

    const filebox = sayable
    // ...
  }
})

From the above code, you can see that we can check whether a Sayable is a valid FileBox.

After we get the FileBox out of the post, we can continue checking its type to filter the Image out.

BTW: you can see the Sayable has been defined at:

type Sayable =
| ContactInterface
| DelayInterface
| FileBoxInterface
| LocationInterface
| MessageInterface
| MiniProgramInterface
| number
| PostInterface
| string
| UrlLinkInterface

And remember to pay attention that: the "Wechaty Sayable" is different from the "Puppet Sayable": the "Puppet Sayable" is a "Redux Action" created by an action creator (typesafe-actions in our code base).

I hope the above code and explanations can answer your question. Please feel free to let me know if you have more questions.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 15, 2022

I think I understand most of your design now. This might be the last question bothering me: What's the border between a post and a regular message?
If we are designing an API for sending image messages. There could be more than one image on each request, so if we receive one image, this is a file message (or image message), otherwise this will be a post message? (as it contains multiple sayable?)

@huan
Copy link
Member

huan commented Apr 15, 2022

TL;DR: the Post is a container for containing multiple Messages (sayables), while a message can only contains one.

You can learn more from the below issue, by searching "container":

I hope those information can make the border clearer.

Please feel free to let me know if you have any future questions!

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 15, 2022

Got it. I'll rework my quote message design.
BTW any plan on use post all along in the future? Since post can contain one or more sayable, why do we still need regular message?

@huan
Copy link
Member

huan commented Apr 15, 2022

Glad to know that we have answered all your questions!

use post all along in the future

If you dig into the code, you will realized that the message is the component of the post.

So I think the best practice would be:

  1. Use message for a single sayable
  2. Use post for multiple sayables

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 15, 2022

I am aware that. However if the only difference is the number of sayables, why don't we just use post?

@huan
Copy link
Member

huan commented Apr 15, 2022

A proposal would be welcome, we can think about it in v2.

If you do the proposal, please be aware about the breaking changes and the convenience.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 18, 2022

I just read through the post code in wechaty and wechaty-puppet. It seems there's nothing much need to change beside adding a message type in puppet.types.Post for quoted message and multiple message. Post.reply() and say(Post) has already been implemented.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 18, 2022

As we added this Message type Post, the puppet can handle it properly. There seems no more work required in wechaty / puppet / puppet-service or grpc. The next step is for puppet implementation to handle messageSendPost.

@hcfw007
Copy link
Member Author

hcfw007 commented May 27, 2022

@huan I just noticed that only Post messages can call messge.toPost(). I'm wondering what the expected user code when quoting a regular message should be. In my idea it should be like this:

      const messagePost = message.toPost()
      const post = bot.Post.builder()
      post.add('quoted')
      post.reply(messagePost)
      post.type(PostType.Message)
      message.say(await post.build())

@su-chang
Copy link
Member

It seems that we should add post.type(PostType.Message)

/**
 * Huan(202201): numbers must be keep unchanged across versions
 */
enum PostType {
  Unspecified = 0,
  Moment  = 1,  // <- WeChat Moments (朋友圈)
  Channel = 2,  // <- WeChat Channel (视频号)
  Message = 3,  // Quoted Message or Muitiplepart Message
}

@hcfw007
Copy link
Member Author

hcfw007 commented May 31, 2022

@huan ping

@nelsonz
Copy link

nelsonz commented Nov 24, 2022

Hi there,

Did quote feature ever get added? I have been trying to get it working using Post, according to @hcfw007 and @su-chang 's last comments, but can't seem to get it working. Any pointers are appreciated! Thanks :)

@hcfw007
Copy link
Member Author

hcfw007 commented Dec 13, 2022

Hi there,

Did quote feature ever get added? I have been trying to get it working using Post, according to @hcfw007 and @su-chang 's last comments, but can't seem to get it working. Any pointers are appreciated! Thanks :)

We have added this feature in our version of wechaty (@juzi/wechaty). We will start working on merging our version into main comunity version soon.

@hcfw007
Copy link
Member Author

hcfw007 commented Apr 26, 2023

It's been a while since last time we talked about this.
I'm still a fan of refractor current messageSendText method.
In our approach, we use a say options to pass quote and at parameters.

interface SayOptionsObject {
  mentionList?: (ContactInterface | '@all')[],
  quoteMessage?: MessageInterface,
}

export const isSayOptionsObject = (target: any) => {
  return (typeof target === 'object'
    && ((target.mentionList && target.mentionList.every((c: any) => ContactImpl.valid(c)))
      || (target.quoteMessage && MessageImpl.valid(target.quoteMessage))
    )
  )
}

type SayOptions = (ContactInterface | '@all') | (ContactInterface | '@all')[] | SayOptionsObject

export type {
  SayableSayer,
  Sayable,
  SayOptions,
  SayOptionsObject,
}

The reason I insist using regular text message here isntead of post are:

  1. Post is disgned to handle mutiple sayables, but here we can only have one sayables.
  2. There is not a clear line between Post and regualr text. I mean, if we use Post here, every message can be a post, containing one sayable, because any message can be quoted by other messages.
  3. If one day, we find that IMs can send quote messages with mutiple sayables (e.g. quote and say an Image and some text at one single message), that's where Post shines.

To sum up, I think the key difference, and the reason regualr message is not Post, is Post should only be used when there are mutiple sayables.

@su-chang
Copy link
Member

su-chang commented May 4, 2023

To sum up, I think the key difference, and the reason regualr message is not Post, is Post should only be used when there are mutiple sayables.

I think @huan has agreed with it, see: #2387 (comment)

So I think the best practice would be:

  1. Use message for a single sayable
  2. Use post for multiple sayables

In my opinion, using Post and Message to implement reply is both okay. However, Message will be more clear and easy to use.

  • Base Message
wechaty.on('message', async msg => {
  await msg.say('This is reply msg', { quoteMessage: msg })
})
  • Base Post
wechaty.on('message', async msg => {
	const messagePost = msg.toPost()
	const post = bot.Post.builder()
	post.add('This is reply msg')
	post.reply(messagePost)
	post.type(PostType.Message)
	message.say(await post.build())
})

@huan
Copy link
Member

huan commented May 5, 2023

How about

wechaty.on('message', async msg => {
  await msg.reply('This is reply msg')
})

@hcfw007
Copy link
Member Author

hcfw007 commented May 5, 2023

After discussion in Wechaty Community Meeting (brief here)
We have dicided to achieve quote message with post.
TODO:

  1. All messages should be able to be parsed as Post in puppet implementation. So you should be able to call postRawPayload with a valid message id and get a valid response.
  2. Implement message.reply as syntactic sugar for @su-chang 's example code above.

@wenzai007
Copy link

Hey @hcfw007 , would like to ask is this quote feature landed in any version of wechaty, if so, is there any example usage that I can reference to see how I can get the quoted original message?

Copy link

dosubot bot commented Jan 30, 2024

Hi, @hcfw007,

I'm helping the wechaty team manage their backlog and am marking this issue as stale. From what I understand, you raised an issue requesting support for quote messages in Wechaty, with discussions and proposals on the implementation methods, including the use of the Post API for handling quote messages. There are ongoing efforts to finalize the implementation and ensure compatibility with the puppet implementation.

Could you please confirm if this issue is still relevant to the latest version of the wechaty repository? If it is, please let the wechaty team know by commenting on the issue. Otherwise, feel free to close the issue yourself, or the issue will be automatically closed in 7 days. Thank you!

@dosubot dosubot bot added the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Jan 30, 2024
@huan huan self-assigned this Jan 31, 2024
@dosubot dosubot bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 6, 2024
@dosubot dosubot bot removed the stale Issue has not had recent activity or appears to be solved. Stale issues will be automatically closed label Feb 6, 2024
@MarkShawn2020
Copy link

After discussion in Wechaty Community Meeting (brief here) We have dicided to achieve quote message with post. TODO:

  1. All messages should be able to be parsed as Post in puppet implementation. So you should be able to call postRawPayload with a valid message id and get a valid response.
  2. Implement message.reply as syntactic sugar for @su-chang 's example code above.

it seems the latest of padlocal still CAN NOT get the original id of the quoted message.

    "wechaty": "^1.20.2",
    "wechaty-puppet-padlocal": "^1.20.1"

@huan huan reopened this Apr 19, 2024
@huan
Copy link
Member

huan commented Apr 19, 2024

@dosubot you should not close an issue which has an assignee.

Copy link

dosubot bot commented Apr 19, 2024

The quote feature has indeed been implemented using the Post API as part of the Wechaty framework. This implementation allows for the parsing of any message into a Post, enabling access to its components, including quoted messages. However, it appears there might be a challenge with the padlocal puppet in retrieving the original ID of the quoted message based on your experience.

To address this, please ensure that you are using the latest versions of the wechaty and wechaty-puppet-padlocal packages. Updates and fixes are regularly made, which might include enhancements to the quote feature and its functionality.

If you're already on the latest versions and still facing issues with accessing the original ID of the quoted message, I recommend raising a specific issue in the repository. When reporting this, include detailed information about the behavior you're encountering, such as any error messages or unexpected outputs. This detailed report will aid the maintainers in investigating and resolving the issue more efficiently.

About Dosu

This response is meant to be useful and save you time. It is not meant to be a precise solution, but rather a starting point for your own research.

Help me be more useful! Please leave a 👍 if this is helpful and 👎 if it is irrelevant.

If you want to continue the conversation, start your reply with @dosu-bot.

@MarkShawn2020
Copy link

After discussion in Wechaty Community Meeting (brief here) We have dicided to achieve quote message with post. TODO:

  1. All messages should be able to be parsed as Post in puppet implementation. So you should be able to call postRawPayload with a valid message id and get a valid response.
  2. Implement message.reply as syntactic sugar for @su-chang 's example code above.

it seems the latest of padlocal still CAN NOT get the original id of the quoted message.

    "wechaty": "^1.20.2",
    "wechaty-puppet-padlocal": "^1.20.1"

I finally got the correct answer:
wechaty-puppet-padlocal should be updated to return a better data structure of quoted message.
see: Quoted message need to be recursive call · Issue #68 · wechaty/puppet-padlocal, wechaty/puppet-padlocal#68

and my clone && debug:
image

@MarkShawn2020
Copy link

wechaty.on('message', msg => {
  const post = await wechaty.Post.builder()
    .add('this is the reply to a quoted messsage')
    .build()

  post.reply(msg) // I'm thinking about to rename the `post.reply()` to `post.replyTo()` so that it will be less confusing.
  await wechaty.publish(post)
})

I cannot make it work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants