whatsapp-web.js icon indicating copy to clipboard operation
whatsapp-web.js copied to clipboard

feat: `Channel`

Open alechkos opened this issue 1 year ago • 79 comments

Table of Contents

- Description

- Related Issues

- Usage Example

- I Want to Test this PR

- I Got an Error While Testing This PR ❌

- How Has the PR Been Tested (latest test on 29.09.2024)

- Types of Changes


Description

The PR introduces new functionality for managing channels.

Added new methods:

  • Client.createChannel that takes 2 parameters:

    • title (required)
    • an object options (optional) with properties:
      • description for a channel description
      • picture for a channel profile picture which will be set upon a channel creation
  • Client.subscribeToChannel to subscribe to channel that takes a channel ID

  • Client.unsubscribeFromChannel to unsubscribe from channel. The method takes 2 parameters:

    • channelId (required)
    • an optional object options with property deleteLocalModels (default value is true). If true, after an unsubscription, it will completely remove a channel from the client channel collection making it seem like the current user have never interacted with it. Otherwise it will only remove a channel from the list of channels the current user is subscribed to and will set the membership type for that channel to GUEST
  • Client.searchChannels to search channels, the method takes 5 optional parameters:

    • searchText, the text by which you want to find the channel (empty string by default)
    • countryCodes, an array of country codes in ISO 3166-1 alpha-2 standart to search for channels created in these countries, your local region is used as a value by default
    • skipSubscribedNewsletters, a boolean value (default value is false). If true, channels that user is subscribed to won't appear in found channels
    • view for specifing the category of channels to get, valid values are:
      • 0 for RECOMMENDED channels (default value)
      • 1 for TRENDING channels
      • 2 for POPULAR channels
      • 3 for NEW channels
    • limit for specifing the limit of found channels to be appear in the returnig results (default value is 50)
  • Updated Client.getChatById to retrieve a Channel instance by its ID

  • Client.getChannelByInviteCode to retrieve a Channel instance by its invite code (that comes after http­s://whatsapp.com/channel/), e.g.: 0029Va4K0PZ5a245NkngBA2M (an invite code of a WhatsApp channel)

  • Client.getChannels that returns all your cached Channel objects as an array

  • Client.sendChannelAdminInvite and Channel.sendChannelAdminInvite to send a channel admin invitation to a user, allowing them to become an admin of the channel, you can also provide a text comment that will be sent along within invitation by providing a string in a comment property of an optional options object

  • Client.acceptChannelAdminInvite and Channel.acceptChannelAdminInvite to accept a channel admin invitation and promote the current user to a channel admin

  • Client.revokeChannelAdminInvite and Channel.revokeChannelAdminInvite to revoke a channel admin invitation previously sent to a user by a channel owner

  • Client.demoteChannelAdmin and Channel.demoteChannelAdmin to demote a channel admin to a regular subscriber (can be used also for self-demotion)

  • Channel.sendMessage and updated Client.sendMessage to send a message to a channel (currently supported message types to send are: text, image, sticker, gif, video, voice and poll)

  • Channel.fetchMessages that works similarly to Chat.fetchMessages

  • Channel.getSubscribers to get subscribers of the channel (only those who are in your contact list), you can pass an optional limit parameter to specify the limit of subscribers to retrieve (if not specified, the limit value will be set to the maximum provided by WhatsApp)

  • Channel.setSubject that updates the channel subject (name)

  • Channel.setDescription that updates the channel description

  • Channel.setProfilePicture that updates the channel propfile picture

  • Channel.setReactionSetting that updates available reactions to use in the channel, valid values to pass:

    • 0 for NONE reactions to be avaliable (turnes off using reactions in a channel)
    • 1 for BASIC reactions to be available: 👍, ❤️, 😂, 😮, 😢, 🙏
    • 2 for ALL reactions to be available
  • Channel.mute and Channel.unmute to mute and unmute channel respectively (once you subcribed to a channel it will be muted)

  • Client.deleteChannel and Channel.deleteChannel that deletes the channel you created

  • Client.transferChannelOwnership and Channel.transferChannelOwnership that transfers a channel ownership to another user. Note: The user you are transferring the channel ownership to must be a channel admin. You can also pass an optional object options with a property shouldDismissSelfAsAdmin (default value is false). If true, after the channel ownership is being transferred to another user, the current user will be dismissed as a channel admin and will become to a channel subscriber.


Related Issues

The PR closes #2094, closes #2529, closes #2538, closes #2551, closes #2952


Usage Example

1. To create a channel:

const { MessageMedia, ... } = require('whatsapp-web.js');

// client initialization...

client.on('ready', async () => {
    let createdChannel = await client.createChannel('ChannelName');
    
    /**
     * The example output of the {@link createdChannel}:
     * {
     *   title: 'ChannelName',
     *   nid: {
     *     server: 'newsletter',
     *     user: 'XXXXXXXXXX',
     *     _serialized: 'XXXXXXXXXX@newsletter'
     *   },
     *   inviteLink: 'https://whatsapp.com/channel/INVITE_CODE',
     *   createdAtTs: 1700002175
     * }
     */
    console.log(createdChannel);

    // You can also provide optional parametes:
    const pic = await MessageMedia.fromUrl(
        'https://i.chzbgr.com/full/9817556992/hAA1BE0BC/bag-12',
        { unsafeMime: true }
    );

    createdChannel = await client.createChannel('ChannelName', {
        description: 'Description',
        picture: pic
    });
});

2. To subscribe to a channel:

// client initialization...

client.on('ready', async () => {
    let channelId = 'XXXXXXXXXX@newsletter';
    // True if the operation completed successfully, false otherwise
    console.log(await client.subscribeToChannel(channelId));
});

3. To unsubscribe from a channel:

// client initialization...

client.on('ready', async () => {
    let channelId = 'XXXXXXXXXX@newsletter';
    // True if the operation completed successfully, false otherwise
    console.log(await client.unsubscribeFromChannel(channelId/* , { deleteLocalModels: true } */));
});

4. To search for channels:

// client initialization...

client.on('ready', async () => {
    const foundChannels = await client.searchChannels({
        searchText: 'StandWithUS',
        countryCodes: ['IL'],
        skipSubscribedNewsletters: true,
        view: 0,
        limit: 5
    });

    /**
     * {
     *   id: {
     *     server: 'newsletter',
     *     user: '120363189916697314',
     *     _serialized: '120363189916697314@newsletter'
     *   },
     *   t: 1697713253,
     *   unreadCount: 0,
     *   unreadDividerOffset: 0,
     *   isReadOnly: true,
     *   muteExpiration: -1,
     *   isAutoMuted: false,
     *   name: 'StandWithUs',
     *   hasUnreadMention: false,
     *   archiveAtMentionViewedInDrawer: false,
     *   hasChatBeenOpened: false,
     *   isDeprecated: false,
     *   pendingInitialLoading: false,
     *   celebrationAnimationLastPlayed: 0,
     *   hasRequestedWelcomeMsg: false,
     *   isGroup: false,
     *   isChannel: true,
     *   channelMetadata: {
     *     id: {
     *       server: 'newsletter',
     *       user: '120363189916697314',
     *       _serialized: '120363189916697314@newsletter'
     *     },
     *     creationTime: 1697713253,
     *     name: 'StandWithUs',
     *     nameUpdateTime: 1697713253815244,
     *     description: 'Supporting Israel And Fighting Antisemitism\n',
     *     descriptionUpdateTime: 1699462060238701,
     *     inviteCode: '0029Va8fZhM05MUj1A47hA0F',
     *     size: 39690,
     *     verified: true,
     *     membershipType: 'guest',
     *     suspended: false,
     *     geosuspended: false,
     *     terminated: false,
     *     messageDeliveryUpdates: [],
     *     geosuspendedCountries: [],
     *     pendingAdmins: [],
     *     subscribers: [],
     *     createdAtTs: 1697713253
     *   },
     *   lastMessage: null
     * }
     */
    console.log(foundChannels[0]); // Outputs the first found channel in an array
});

5. To get a channel by its ID:

// client initialization...

client.on('ready', async () => {
    let channelId = '120363189916697314@newsletter';

    // The output will be a `Channel` instance, if exists:
    console.log(await client.getChatById(channelId));
});

6. To get a channel by its invite code:

// client initialization...

client.on('ready', async () => {
    let channelInviteCode = '0029Va8fZhM05MUj1A47hA0F';

    // The output will be a `Channel` instance, if exists:
    console.log(await client.getChannelByInviteCode(channelInviteCode));
});

7. To get all cached channels:

// client initialization...

client.on('ready', async () => {
    // The output will be an array of your cached `Channel` objects:
    console.log(await client.getChannels());
});

8. To send a channel admin invitation to a user as a channel admin:

// client initialization...

client.on('ready', async () => {
    const userId = '[email protected]';
    const channelId = 'YYYYYYYYYY@newsletter';
    const greeting = 'Hello, please become a channel admin';

    // True if the operation completed successfully, false otherwise
    console.log(await client.sendChannelAdminInvite(userId, channelId, { comment: greeting }));

    // OR
    const channel = await getChatById(channelId);
    await channel.sendChannelAdminInvite(userId, { comment: greeting });
});

9. To accept a channel admin invitation as a regular user:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';

    // True if the operation completed successfully, false otherwise
    console.log(await client.acceptChannelAdminInvite(channelId));

    // OR
    const channel = await getChatById(channelId);
    await channel.acceptChannelAdminInvite();
});

10. To revoke a channel admin invitation as a channel admin:

// client initialization...

client.on('ready', async () => {
    const userId = '[email protected]';
    const channelId = 'YYYYYYYYYY@newsletter';

    // True if the operation completed successfully, false otherwise
    console.log(await client.revokeChannelAdminInvite(channelId, userId));

    // OR
    const channel = await getChatById(channelId);
    await channel.revokeChannelAdminInvite(userId);
});

11. To demote a channel admin:

// client initialization...

client.on('ready', async () => {
    const userId = '[email protected]';
    const channelId = 'YYYYYYYYYY@newsletter';

    // True if the operation completed successfully, false otherwise
    console.log(await client.demoteChannelAdmin(channelId, userId));

    // OR
    const channel = await getChatById(channelId);
    await channel.demoteChannelAdmin(userId);
});

12. To get channel subscribers:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);
    const limit = 5;

    /**
     * The example of an output (in my case there is only one subscriber):
     * 
     * [
     *   {
     *     contact: {
     *       id: {
     *         server: 'c.us',
     *         user: 'XXXXXXXXX',
     *         _serialized: '[email protected]'
     *       },
     *       name: 'Test',
     *       shortName: 'Test',
     *       pushname: 'Test',
     *       type: 'in',
     *       isBusiness: false,
     *       isEnterprise: false,
     *       isSmb: false,
     *       isContactSyncCompleted: 1,
     *       textStatusLastUpdateTime: -1,
     *       isMe: false,
     *       isUser: true,
     *       isGroup: false,
     *       isWAContact: true,
     *       isMyContact: true,
     *       isBlocked: false,
     *       userid: 'XXXXXXXXX'
     *     },
     *     role: 'subscriber'
     *   }
     * ]
     */
    console.log(await channel.getSubscribers(limit));
});

13. To set channel subject (name):

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);
    const newSubject = 'NewSubject';
    // True if the operation completed successfully, false otherwise
    console.log(await channel.setSubject(newSubject));
});

14. To set channel description:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);
    const newDescription = 'NewSubject';
    // True if the operation completed successfully, false otherwise
    console.log(await channel.setDescription(newDescription));
});

15. To set channel profile picture:

const { MessageMedia, ... } = require('whatsapp-web.js');

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);
    const newPicture = await MessageMedia.fromUrl(
        'https://i.redd.it/l9vklw5gh4841.jpg',
        { unsafeMime: true }
    );
    // True if the operation completed successfully, false otherwise
    console.log(await channel.setProfilePicture(newPicture));
});

16. To set a channel available reactions:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);

    // True if the operation completed successfully, false otherwise
    console.log(await channel.setReactionSetting(0)); // Allows all reactions
    console.log(await channel.setReactionSetting(1)); // Allows default reactions (👍, ❤️, 😂, 😮, 😢, 🙏)
    console.log(await channel.setReactionSetting(2)); // Turns off reactions
});

17. To mute/unmute a channel:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const channel = await getChatById(channelId);

    // True if the operation completed successfully, false otherwise
    console.log(await channel.unmute());
    console.log(await channel.mute());
});

18. To delete a channel:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    // True if the operation completed successfully, false otherwise
    console.log(await client.deleteChannel(channelId));

    // OR
    const channel = await getChatById(channelId);
    await channel.deleteChannel();
});

19. To transfer a channel ownership:

// client initialization...

client.on('ready', async () => {
    const channelId = 'YYYYYYYYYY@newsletter';
    const newChannelOwnerId = '[email protected]';
    // True if the operation completed successfully, false otherwise
    console.log(await client.transferChannelOwnership(channelId, newChannelOwnerId));

    /**
     * The same but after the channel ownership is being transferred to another user,
     * the current user will be dismissed as a channel admin and will become to a channel subscriber.
     */
    console.log(await client.transferChannelOwnership(channelId, newChannelOwnerId, { shouldDismissSelfAsAdmin: true }));

    // OR through the 'Channel' object:
    const channel = await getChatById(channelId);
    await channel.transferChannelOwnership(newChannelOwnerId);
});

To test this PR by yourself you can run one of the following commands:

  • NPM
npm install github:alechkos/whatsapp-web.js#channels
  • YARN
yarn add github:alechkos/whatsapp-web.js#channels

If you encounter any errors while testing this PR, please provide in a comment:

  1. The code you've used without any sensitive information (use syntax highlighting for more readability)
  2. The error you got
  3. The library version
  4. The WWeb version: console.log(await client.getWWebVersion());
  5. The browser (Chrome/Chromium)

[!IMPORTANT] You have to reapply the PR each time it is changed (new commits were pushed since your last application)


How Has The PR Been Tested (latest test on 29.09.2024)

Tested with a code provided in usage example.

Tested On:

Environment:

  • Android 10:
    • Types of accounts:
      • Standart: latest
      • WhatsApp Business: latest
  • Windows 11:
    • WWebJS: v1.26.1-alpha.1
    • WWeb: v2.3000.1016898497
    • Puppeteer: v18.2.1
    • Node.js: 20.11.0
    • Google Chrome: latest

Types of Changes

  • [ ] Dependency change
  • [ ] Bug fix (non-breaking change which fixes an issue)
  • [X] New feature (non-breaking change which adds functionality)
  • [ ] Breaking change (fix/feature that would cause existing functionality to change)

Checklist

  • [X] My code follows the code style of this project
  • [ ] I have updated the usage example accordingly (example.js)
  • [X] I have updated the documentation accordingly (index.d.ts)

alechkos avatar Nov 02 '23 17:11 alechkos

sending photo in channels has some problems

bruninoit avatar Nov 08 '23 12:11 bruninoit

@bruninoit

sending photo in channels has some problems

wanna tell me what they are?

alechkos avatar Nov 09 '23 05:11 alechkos

wanna tell me what they are?

After sending an image in a channel... If you access the channel from the channel owner and try to press the button to send the image again, WhatsApp crashes If you access the channel from a channel subscriber and try to download the image, it won't download. I used the exact same code to send a photo in private chat and it works correctly, while sending in a channel there are these problems.

bruninoit avatar Nov 09 '23 15:11 bruninoit

Tested and worked great here. Here is how I did it.

Step 1: Install @alechkos wweb.js version:

npm install github:alechkos/whatsapp-web.js#channels --save

Step 2: Create a channel and get it's ID:

const channelId = (await client.createChannel('MyChannel))?.nid._serialized
console.log('My channel id is:', channelId)

Step 3: Start publishing:

await client.sendMessage(channelId, 'Hello World')

devsakae avatar Nov 09 '23 18:11 devsakae

One question, tho. How to publish media?

devsakae avatar Nov 09 '23 18:11 devsakae

@devsakae

One question, tho. How to publish media?

sending media is still doesn't work

alechkos avatar Nov 09 '23 18:11 alechkos

I used the exact same code to send a photo in private chat and it works correctly, while sending in a channel there are these problems.

Same here. I even though it was my connection, but every other media downloaded from channels work great.

It's a bug

devsakae avatar Nov 16 '23 19:11 devsakae

@devsakae @bruninoit

Thank you for testing, sendMessage has been fixed and now supports these message types to send: text, image, sticker, gif, or video

alechkos avatar Nov 21 '23 06:11 alechkos

Care to share how I install the latest changes?

I did npm install (I usually go with npm ci), but it didn't downloaded the changes made https://github.com/pedroslopez/whatsapp-web.js/pull/2620/commits/ab40eddf6d5fff7517a143f6c03db13f3254ba7b

devsakae avatar Nov 22 '23 18:11 devsakae

@devsakae

how I install the latest changes?

npm install github:alechkos/whatsapp-web.js#channels

alechkos avatar Nov 22 '23 18:11 alechkos

Of course.. 🫥

Ty

devsakae avatar Nov 22 '23 18:11 devsakae

I get an error when trying to run client.getChats():

            throw new Error('Evaluation failed: ' + helper_js_1.helper.getExceptionMessage(exceptionDetails));
                  ^

Error: Evaluation failed: TypeError: Cannot read properties of undefined (reading 'isChannel')
    at Object.window.WWebJS.getChatOrChannelModel (__puppeteer_evaluation_script__:404:67)
    at __puppeteer_evaluation_script__:394:62
    at Array.map (<anonymous>)
    at Object.window.WWebJS.getChats (__puppeteer_evaluation_script__:394:36)
    at __puppeteer_evaluation_script__:2:40
    at ExecutionContext._evaluateInternal (/Users/tobimori/Developer/vierbeiner-in-not/node_modules/.pnpm/[email protected]/node_modules/puppeteer/src/common/ExecutionContext.ts:273:13)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at ExecutionContext.evaluate (/Users/tobimori/Developer/vierbeiner-in-not/node_modules/.pnpm/[email protected]/node_modules/puppeteer/src/common/ExecutionContext.ts:140:12)
    at async Client.getChats (/Users/tobimori/Developer/vierbeiner-in-not/node_modules/.pnpm/github.com+alechkos+whatsapp-web.js@6b18bd1260164167d0354ba21501a049065320d2/node_modules/whatsapp-web.js/src/Client.js:974:21)
    at get (/Users/tobimori/Developer/vierbeiner-in-not/whatsapp/routes/chats.ts:10:17)

tobimori avatar Nov 24 '23 14:11 tobimori

@tobimori

I get an error when trying to run client.getChats()

Thank you, fixed

alechkos avatar Nov 24 '23 15:11 alechkos

Could you add inviteLink on response of Channel

fukumori avatar Nov 28 '23 20:11 fukumori

@fukumori

Could you add inviteLink on response of Channel

What do you mean?

alechkos avatar Nov 28 '23 20:11 alechkos

How can one delete a message in a channel?

Seems deleting the message by id doesn't work. While I do not get an error, the message is still available.

themazim avatar Dec 01 '23 16:12 themazim

@themazim

How can one delete a message in a channel?

Seems deleting the message by id doesn't work. While I do not get an error, the message is still available.

I will check it

alechkos avatar Dec 01 '23 16:12 alechkos

@themazim

How can one delete a message in a channel?

You can do it just like in regular chat:

// ...
const msg = await client.getMessageById(msgId);
await msg.delete(true);
// ...

alechkos avatar Dec 01 '23 21:12 alechkos

@themazim

How can one delete a message in a channel?

You can do it just like in regular chat:

// ...
const msg = await client.getMessageById(msgId);
await msg.delete(true);
// ...

Thanks a lot. This actually helped. I set everyone: false which simply doesn't seem to work with channels.

themazim avatar Dec 01 '23 21:12 themazim

Hi, can anyone check if changing message caption works? I've tried but can only get it to work with simple text messages (no media).

This is my code:

//...
const msg = await client.getMessageById(msgId);
await msg.edit(caption);
//...

roockisgreat avatar Dec 03 '23 08:12 roockisgreat

@roockisgreat

I've tried but can only get it to work with simple text messages (no media)

Check the PR that adds that functionality, also I have not checked the message editing in channels yet

alechkos avatar Dec 03 '23 13:12 alechkos

Thank you very much, I confirm that with these changes the editing media caption works. (PR)

roockisgreat avatar Dec 03 '23 15:12 roockisgreat

Hi, i'm here again sorry. The subscribeToChannel method doesn't work, it returns an error: (node:15308) UnhandledPromiseRejectionWarning: Error: Evaluation failed: i at ExecutionContext._evaluateInternal Furthermore, the unsubscribeFromChannel method does not return any errors, but I always remain subscribed to the channel (even using the deleteLocalModels option). Final question, if I'm not subscribed to a channel, is the getChannelByInviteCode method normal that returns undefined?

roockisgreat avatar Dec 04 '23 17:12 roockisgreat

@roockisgreat I will check

alechkos avatar Dec 04 '23 17:12 alechkos

Can someone check if sending a link to a channel would load the preview (the preview of the link)? (not working for me)

carlvallory avatar Dec 04 '23 18:12 carlvallory

@carlvallory

Can someone check if sending a link to a channel would load the preview (the preview of the link)? (not working for me)

It won't, it needs to be implemented also

alechkos avatar Dec 04 '23 20:12 alechkos

@roockisgreat

The subscribeToChannel method doesn't work, it returns an error. Furthermore, the method does not return any errors, but I always remain subscribed to the channel (even using the deleteLocalModels option). If I'm not subscribed to a channel, is the Client.getChannelByInviteCode method normal that returns undefined?

Thank you for testing :) I fixed some functionality, now Client.subscribeToChannel, Client.unsubscribeFromChannel and Client.getChannelByInviteCode methods work properly

alechkos avatar Dec 05 '23 04:12 alechkos

So, I confirm that the Client.unsubscribeFromChannel method works. But the Client.subscribeToChannel method does not work yet (this window.WWebJS.getChat(channelId, { getAsModel: false }) return undefined). The Client.getChannelByInviteCode still return undefined if you are not subscribe to channel, is it normal?

roockisgreat avatar Dec 05 '23 07:12 roockisgreat

@roockisgreat Did you run this before the new test?

npm install github:alechkos/whatsapp-web.js#channels

Also provide your code

alechkos avatar Dec 05 '23 11:12 alechkos

Yes, before the new test i run:

npm uninstall whatsapp-web.js
npm install github:alechkos/whatsapp-web.js#channels

My code for getChannelByInviteCode:

const invite_code = "xxxxxx"; // 'https://whatsapp.com/channel/xxxxxx'
const channel = await client.getChannelByInviteCode(invite_code);
console.log(channel); //return undefined

My code for subscribeToChannel:

let res = await client.subscribeToChannel("xxxxxx@newsletter");
console.log(res) //return false because window.WWebJS.getChat(channelId, { getAsModel: false }) return undefined

The unsubscribeFromChannel methode work perfect instead now.

EDIT: For the getChannelByInviteCode method the problem is ChatFactory.create(this, result) (with the options.getMetadata = true return the data)

EDIT 2: For the subscribeToChannel method the problem seems the function window.WWebJS.getChat. Inside this function the methods window.Store.NewsletterCollection.get(chatId) and await window.Store.ChannelUtils.queryAndUpdateNewsletterMetadataAction(chatId,{ fields: fields }) return undefined.

roockisgreat avatar Dec 05 '23 14:12 roockisgreat