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

feat: Channel #2620

Open
wants to merge 72 commits into
base: main
Choose a base branch
from
Open

feat: Channel #2620

wants to merge 72 commits into from

Conversation

alechkos
Copy link
Collaborator

@alechkos alechkos commented Nov 2, 2023

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 22.04.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 object options (optional) with property deleteLocalModels. If true, after an unsubscription, it will completely remove a channel from the 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
    • view for specifing the category of channels to get, valid values are:
      • RECOMMENDED (default value)
      • MOST_ACTIVE
      • POPULAR
      • NEW
      • FEATURED
    • limit for specifing the limit of found channels to be appear in the returnig results (default value is 50)
    • an optional object sortOptions to sort the returning results by with these properties:
      • field for specifing the field to sort by, valid values are:
        • SUBSCRIBERS (default value)
        • CREATION_TIME
      • order, valid values are:
        • DESCENDING (default value)
        • ASCENDING
  • 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 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 ALL reactions to be available
    • 1 for BASIC reactions to be available: 👍, ❤️, 😂, 😮, 😢, 🙏
    • 2 for NONE reactions to be avaliable (turnes off using reactions in a channel)
  • Channel.mute and Channel.unmute to mute and unmute channel respectively (once you subcribed to a channel it will be muted)

  • Channel.deleteChannel that deletes the channel you created


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',
        sortOptions: {
            order: true
        },
        countryCodes: ['IL']
    });

    /**
     * {
     *   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 = 'XXXXXXXXXX@c.us';
    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 = 'XXXXXXXXXX@c.us';
    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 = 'XXXXXXXXXX@c.us';
    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: 'XXXXXXXXX@c.us'
     *       },
     *       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';
    const channel = await getChatById(channelId);

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

To test this PR by yourself you should do two steps:

1. Install the PR by running one of the following commands:

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

2. Lock your WWeb version on 2.2412.54:

const wwebVersion = '2.2412.54';

const client = new Client({
    authStrategy: new LocalAuth(), // your authstrategy here
    puppeteer: {
        // puppeteer args here
    },
    // locking the wweb version
    webVersionCache: {
        type: 'remote',
        remotePath: `https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/${wwebVersion}.html`,
    },
});

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 22.04.2024)

Tested with a code provided in usage example.

Tested On:

Types of accounts:

  • Personal
  • Buisness

Environment:

  • Android 10:
    • WhatsApp: latest
    • WA Business: latest
  • Windows 10:
    • WWebJS: v1.23.1-alpha.5
    • WWeb: v2.2412.54
    • Puppeteer: v18.2.1
    • Node: v18.17.1
    • Chrome: latest

Types of Changes

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

Checklist

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

@bruninoit
Copy link

sending photo in channels has some problems

@alechkos
Copy link
Collaborator Author

alechkos commented Nov 9, 2023

@bruninoit

sending photo in channels has some problems

wanna tell me what they are?

@bruninoit
Copy link

bruninoit commented Nov 9, 2023

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.

@devsakae
Copy link

devsakae commented Nov 9, 2023

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
Copy link

devsakae commented Nov 9, 2023

One question, tho. How to publish media?

@alechkos
Copy link
Collaborator Author

alechkos commented Nov 9, 2023

@devsakae

One question, tho. How to publish media?

sending media is still doesn't work

@devsakae
Copy link

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

@alechkos
Copy link
Collaborator Author

alechkos commented Nov 21, 2023

@devsakae @bruninoit

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

@zerbfra

This comment was marked as outdated.

@iocodz

This comment was marked as resolved.

@carlvallory

This comment was marked as resolved.

@alechkos alechkos marked this pull request as draft April 16, 2024 02:53
Repository owner locked and limited conversation to collaborators Apr 16, 2024
Repository owner unlocked this conversation Apr 19, 2024
src/util/Injected.js Outdated Show resolved Hide resolved
Co-authored-by: Sven Neumann <s.neumann@websix.de>
@alechkos alechkos marked this pull request as ready for review April 22, 2024 05:39
@alechkos
Copy link
Collaborator Author

alechkos commented May 1, 2024

Now you can send polls in channels:

const { Client, Poll, /* others */ } = require('whatsapp-web.js');

// client initialization...

const channelId = 'your_channel_id@newsletter';

client.on('ready', async () => {
    await client.sendMessage(channelId, new Poll('Cats or Dogs?', ['Cats', 'Dogs']));

    // Also you can send polls with multiple answers:
    await client.sendMessage(channelId, new Poll('Cats or Dogs?', ['Cats', 'Dogs'], { allowMultipleAnswers: true }));
});

@shirser121
Copy link
Collaborator

Should we support get channel by using getChatById?

@alechkos
Copy link
Collaborator Author

alechkos commented May 2, 2024

@shirser121

Should we support get channel by using getChatById?

Definitely yes

const sendChannelMsgResponse = await window.Store.SendChannelMessage.sendNewsletterMessageJob({
msg: msg,
type: message.type === 'chat' ? 'text' : isMedia ? 'media' : 'pollCreation',
newsletterJid: chat.id.toJid(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should allow some extra option so pepole can control some fileds

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shirser121

Maybe we should allow some extra option so pepole can control some fileds

Unfortunately, once you you change those props, the message is not being sent.
But I have not tested it a lot.
You can try to play with adding some extra properties. If it will work for you, just suggest your changes and I'll add them as your commit :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shirser121
Don't we already have extra options here?

...(isMedia ? { mediaMetadata: msg.avParams() } : {})
});

if (sendChannelMsgResponse.success) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If success is false, what happened?

Copy link
Collaborator Author

@alechkos alechkos May 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shirser121

If success is false, what happened?

Once the response is success, we update the timestamp and serverId props

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise we don't

@caiotbc

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
waiting for testers Waiting for other people test this PR in other envs
Projects
None yet