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: Community #2531

Draft
wants to merge 73 commits into
base: main
Choose a base branch
from
Draft

feat: Community #2531

wants to merge 73 commits into from

Conversation

alechkos
Copy link
Collaborator

@alechkos alechkos commented Sep 23, 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 30.11.2023)

- Types of Changes


Description

The PR introduces new functionality for managing communities.

By default, a community consists of two components: the parent group and its default subgroup, which serves as an announcement group. The default subgroup is represented by an instance of GroupChat as it functions like a typical "closed" group. The parent group, however, is an instance of the Community class, and all community-related operations are carried out on it.

The Community class extends the GroupChat class, making all the methods available to the Community class as well.

You can simply retrieve a parent group object or its subgroup object.

  1. Updated events to support communities:
    • group_join event: is now emitted also when a user joins community linked subgroups
    • group_admin_changed event: is now emitted also when a user is promoted/demoted in community linked subgroups
  2. Added new methods:
    • Client.createCommunity that takes required parameter title and can also take an optional object options with properties:
    • Community.linkSubgroups that takes a single group ID or an array of group IDs to link to the community
    • Community.unlinkSubgroups, the same as previous but for group unlinking, can also take an optional object options with a boolean property removeOrphanMembers (false by default). If true, the method will remove specified subgroups from the community along with their members who are not part of any other subgroups within the community
    • Client.joinSubgroup and Community.joinSubgroup methods to join a community subgroup by community ID and a subgroup ID
    • Community.promoteParticipants and Community.demoteParticipants now supports communities
    • Community.removeParticipants method now supports communities

      NOTE:
      Removing participant from the community will remove them also from all its linked groups

    • Community.getParticipants method to retrieve community participants

      NOTE:
      To get the full result, you need to be a community admin.
      Otherwise, you will only get the participants that a regular community member can see

    • Community.getSubgroups method to retrieve the list of community subgroups
    • Community.getJoinedSubgroups method to retrieve the community subgroups the current user is a member of
    • Community.getUnjoinedSubgroups method to retrieve all community subgroups in which the current user is not yet a member, preferable to use in a case when the current user is not a community owner/admin but only a member, otherwise, the result will be an empty array
    • Community.setNonAdminSubGroupCreation method that sets who can add groups to the community (only admins or all members)
    • Client.deactivateCommunity and Community.deactivate methods for community creators

      NOTE:
      The method is only for community owners/admins to allow them to delete the community
      Community admins can't deactivate the community, but can only leave it,
      while community creators can't leave the community but can only deactivate it instead

    • Community.leave method now supports communities

      NOTE:
      If you're community admin or a regular member, you can leave the community by leaving through its parent group
      or also through its default subgroup


Related Issues

The PR closes #2533, closes #2483


Usage Example

1. To get the community object (the parent group, is an instance of Community):

let community;
community = await client.getChatById('[email protected]');
// OR
[community] = (await client.getChats()).filter(chat => chat.isCommunity && chat.name === 'CommunityName');
/**
 * The example output of the {@link community}:
 * {
 *   groupMetadata: {
 *     id: {
 *       server: 'g.us',
 *       user: 'XXXXXXXXXX',
 *       _serialized: '[email protected]'
 *     },
 *     creation: 16775XXXXXX,
 *     subject: 'CommunityName',
 *     subjectTime: 16775XXXXXX,
 *     descTime: 0,
 *     restrict: false,
 *     announce: false,
 *     noFrequentlyForwarded: false,
 *     ephemeralDuration: 0,
 *     membershipApprovalMode: false,
 *     memberAddMode: 'admin_add',
 *     reportToAdminMode: false,
 *     size: 0,
 *     support: false,
 *     suspended: false,
 *     terminated: false,
 *     uniqueShortNameMap: {},
 *     isLidAddressingMode: false,
 *     isParentGroup: true,
 *     isParentGroupClosed: true,
 *     defaultSubgroup: {
 *       server: 'g.us',
 *       user: 'YYYYYYYYYY',
 *       _serialized: '[email protected]'
 *     },
 *     allowNonAdminSubGroupCreation: false,
 *     lastActivityTimestamp: 0,
 *     lastSeenActivityTimestamp: 0,
 *     incognito: false,
 *     participants: [
 *      *   {
        *     id: {
        *       server: 'c.us',
        *       user: 'ZZZZZZZZZZ',
        *       _serialized: '[email protected]'
        *     },
        *     isAdmin: true,
        *     isSuperAdmin: false
        *   },
        *   ...
 *     ],
 *     pendingParticipants: [],
 *     pastParticipants: [],
 *     membershipApprovalRequests: [],
 *     subgroupSuggestions: [],
 *     isDefaultSubgroup: false
 *   },
 *   isCommunity: true,
 *   id: {
 *     server: 'g.us',
 *     user: 'XXXXXXXXXX',
 *     _serialized: '[email protected]'
 *   },
 *   name: 'CommunityName',
 *   isGroup: true,
 *   isReadOnly: false,
 *   unreadCount: 0,
 *   timestamp: undefined,
 *   archived: undefined,
 *   pinned: false,
 *   isMuted: true,
 *   muteExpiration: -1,
 *   lastMessage: undefined
 * }
 */

2. To get the default community subgroup object (is an instance of GroupChat):

let subgroup;
subgroup = await client.getChatById('[email protected]');
// OR
[subgroup] = (await client.getChats()).filter(chat => chat?.groupMetadata.isDefaultSubgroup && chat.name === 'CommunityName');
/**
 * The example output of the {@link subgroup}:
 * {
 *   groupMetadata: {
 *     id: {
 *       server: 'g.us',
 *       user: 'XXXXXXXXXX',
 *       _serialized: '[email protected]'
 *     },
 *     creation: 16775XXXXX,
 *     subject: 'CommunityName',
 *     subjectTime: 16775XXXXX,
 *     descTime: 0,
 *     restrict: false,
 *     announce: true,
 *     noFrequentlyForwarded: false,
 *     ephemeralDuration: 0,
 *     membershipApprovalMode: false,
 *     memberAddMode: 'admin_add',
 *     reportToAdminMode: false,
 *     size: 2,
 *     support: false,
 *     suspended: false,
 *     terminated: false,
 *     uniqueShortNameMap: {},
 *     isLidAddressingMode: false,
 *     isParentGroup: false,
 *     isParentGroupClosed: false,
 *     parentGroup: {
 *       server: 'g.us',
 *       user: 'YYYYYYYYYY',
 *       _serialized: '[email protected]'
 *     },
 *     generalSubgroup: false,
 *     generalChatAutoAddDisabled: false,
 *     allowNonAdminSubGroupCreation: false,
 *     lastActivityTimestamp: 0,
 *     lastSeenActivityTimestamp: 0,
 *     incognito: true,
 *     participants: [ ... ],
 *     pendingParticipants: [],
 *     pastParticipants: [ ... ],
 *     membershipApprovalRequests: [],
 *     subgroupSuggestions: [],
 *     isDefaultSubgroup: true
 *   },
 *   id: {
 *     server: 'g.us',
 *     user: 'XXXXXXXXXX',
 *     _serialized: '[email protected]'
 *   },
 *   name: 'CommunityName',
 *   isGroup: true,
 *   isReadOnly: false,
 *   unreadCount: 0,
 *   timestamp: 1696XXXXX,
 *   archived: undefined,
 *   pinned: false,
 *   isMuted: false,
 *   muteExpiration: 0,
 *   lastMessage: undefined
 * }
 */

3. To create the community:

let createdCommunity;
/**
 * The example output of the {@link createdCommunity}:
* {
*   title: 'CommunityName',
*   cid: {
*     server: 'g.us',
*     user: 'ZZZZZZZZZZ',
*     _serialized: '[email protected]'
*   },
*   defaultSubgroup: {
*     server: 'g.us',
*     user: 'WWWWWWWWW',
*     _serialized: '[email protected]'
*   },
*   createdAtTs: timestamp
* }
*/
createdCommunity = await client.createCommunity('CommunityName');

// You can also provide optional parametes:
createdCommunity = await client.createCommunity('CommunityName', {
    description: 'Description',
    subGroupIds: ['[email protected]', '[email protected]', '[email protected]'], // group IDs to link to the community
    membershipApprovalMode: true, // false by default
    allowNonAdminSubGroupCreation: true // false by default
});

4. To link subgroups to the community:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the method execution:
 * {
 *   linkedGroupIds: [ '[email protected]' ],
 *   failedGroups: [
 *     {
 *       groupId: '[email protected]',
 *       code: 409,
 *       message: 'SubGroupConflictError'
 *     },
 *     {
 *       groupId: '[email protected]',
 *       code: 403,
 *       message: 'SubGroupForbiddenError'
 *     }
 *   ]
 * }
 */
console.log(await community.linkSubgroups(['[email protected]', '[email protected]', '[email protected]']));

5. To unlink subgroups from the community:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the unlinking result is as the linking one
 * but also you can provide an optional parameter 'removeOrphanMembers':
 */
console.log(await community.unlinkSubgroups(
    ['[email protected]', '[email protected]', '[email protected]'],
    { removeOrphanMembers: true } // false by default
));

6. To join community subgroup:

let result;

result = await client.joinSubgroup('[email protected]', '[email protected]');
// OR
const community = await client.getChatById('[email protected]');
result = await community.joinSubgroup('[email protected]');

/**
 * Example of the {@link result} object:
 * 
 * {
 *   gid: {
 *     server: 'g.us',
 *     user: 'YYYYYYYYY',
 *     _serialized: '[email protected]'
 *   },
 *   code: 200,
 *   message: 'The membership request was sent or you joined the subgroup successfully'
 * }
 */
console.log(result);

7. To promote/demote community members:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the method execution (the same output structure is for the demoting):
 * [
 *   {
 *     id: {
 *       server: 'c.us',
 *       user: 'YYYYYYYYYY',
 *       _serialized: '[email protected]'
 *     },
 *     code: 403,
 *     message: "The participant can't be promoted, maybe they are not a community member"
 *   },
 *   {
 *     id: {
 *       server: 'c.us',
 *       user: 'ZZZZZZZZZZ',
 *       _serialized: '[email protected]'
 *     },
 *     code: 200,
 *     message: 'The participant was promoted successfully'
 *   }
 * ]
 */
console.log(await community.promoteParticipants(['[email protected]', '[email protected]']));

8. To remove participants:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the method execution (the same output structure is for the participants adding):
 * {
 *   '[email protected]': {
 *     code: 200,
 *     message: 'The participant was removed successfully from the community and its subgroups'
 *   },
 *   '[email protected]': {
 *     code: 409,
 *     message: 'The participant is not a community member'
 *   }
 * }
 */
console.log(await community.removeParticipants(['[email protected]', '[email protected]']));

9. To get community participants:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the method execution:
 * [
 *   {
 *     id: {
 *       server: 'c.us',
 *       user: 'XXXXXXXXXX',
 *       _serialized: '[email protected]'
 *     },
 *     isAdmin: false,
 *     isSuperAdmin: false
 *   },
 *   {
 *     id: {
 *       server: 'c.us',
 *       user: 'YYYYYYYYYY',
 *       _serialized: '[email protected]'
 *     },
 *     isAdmin: true,
 *     isSuperAdmin: false
 *   },
 *   ...
 * ]
 */
console.log(await community.getParticipants());

10. To get community subgroups:

const community = await client.getChatById('[email protected]');
/**
 * The example output of the method execution:
 * [
 *   {
 *     server: 'g.us',
 *     user: 'ZZZZZZZZZZ',
 *     _serialized: '[email protected]'
 *   },
 *   {
 *     server: 'g.us',
 *     user: 'YYYYYYYYYY',
 *     _serialized: '[email protected]'
 *   },
 *   ...
 * ]
 */
console.log(await community.getSubgroups());

11. To get joined subgroups:

const community = await client.getChatById('[email protected]');
/** The example output is the same as in getSubgroups */
console.log(await community.getJoinedSubgroups());

12. To get unjoined subgroups:

const community = await client.getChatById('[email protected]');
/** The example output is the same as in getSubgroups */
console.log(await community.getUnjoinedSubgroups());

13. To set who can add groups to the community (only admins or all members):

const community = await client.getChatById('[email protected]');
await community.setNonAdminSubGroupCreation(true); // all members can add groups to that community
await community.setNonAdminSubGroupCreation(false); // only admins can add groups to that community

14. To deactivate the community:

await client.deactivateCommunity('[email protected]');
// OR
const community = await client.getChatById('[email protected]');
await community.deactivate();

15. To leave the community:

const community = await client.getChatById('[email protected]'/* '[email protected]' */);
await community.leave();

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

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

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

Tested with the 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.0
    • WWeb: v2.2347.56
    • 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)

@alechkos alechkos force-pushed the community branch 2 times, most recently from 47a16fa to 2bef97e Compare September 29, 2023 00:51
@alechkos alechkos force-pushed the community branch 2 times, most recently from a6a547b to e5e15ea Compare September 30, 2023 13:36
@joweste
Copy link

joweste commented Feb 1, 2024

@joweste

How to query the ProfilePicUrl, membershipApprovalMode and allowNonAdminSubGroupCreation of a community by id?

What do you mean "to query"? Go through the description

Never mind. These values settings are in groupMetadata.

@alechkos
Copy link
Collaborator Author

alechkos commented Feb 1, 2024

@joweste

About the getParticipants problem? Is it the same problem, because I am not a community member?

In order to get community members you have at least to be its member, as mentioned here

@joweste
Copy link

joweste commented Feb 1, 2024

@joweste

About the getParticipants problem? Is it the same problem, because I am not a community member?

In order to get community members you have at least to be its member, as mentioned here

Sorry, but the community admin just said me, if I am member at least one group of the community, then I am already member of the community. Does it make sense? If yes, then I should have rights to see all groups of that community and getParticipants. As I said before, I am member in 3 groups in that community

@alechkos
Copy link
Collaborator Author

alechkos commented Feb 1, 2024

@joweste

Sorry, but the community admin just said me, if I am member at least one group of the community, then I am already member of the community.

You are right, my fault.
I have to check the Community.getParticipants method, could be that sonething is broke in the latest wweb versions

@joweste
Copy link

joweste commented Feb 1, 2024

@joweste

Sorry, but the community admin just said me, if I am member at least one group of the community, then I am already member of the community.

You are right, my fault. I have to check the Community.getParticipants method, could be that sonething is broke in the latest wweb versions

Thank you. Could you see why I can´t list all groups in the community where I am member as well?

@joweste
Copy link

joweste commented Feb 2, 2024

Hi,
How can I get the profilePictureUrl of the community?
I have tried to use the community.id._serialized as:

try {
        profilePicUrl = await client.getProfilePicUrl(community.id._serialized);
} catch (error) {
        console.log(error)
        profilePicUrl = "";
}

I also tried to get the picture using the "defaultSubgroup._serialized" from groupMetadata but no look.

@joweste
Copy link

joweste commented Feb 3, 2024

Only as information, if I get the my commonGroups with the community admin user, it shows 10 groups.
For sample:
if 'nn' is the serialized id of the admin communnity user, and I do:

const contact = await client.getContactById(nn);
const commomGroups = await contact.getCommonGroups();

It reports all 10 groups of the community.

But if get the subgroups of the community as:

const community = await client.getChatById('[email protected]');
console.log(await community.getSubgroups());

it reports only 4 groups( where 3 are joined and 1 unjoined)

@joweste
Copy link

joweste commented Feb 3, 2024

Hi, when I get the subgroups participants of a community, I get many @​lid numbers.

For sample:

const groupChat = await client.getChatById(subgroup community id);
const participants = await groupChat.groupMetadata.participants;

Here, 'participants' has many @​lid numbers.

Is possible I get the contact names of this @​lid numbers?

@alechkos
Copy link
Collaborator Author

alechkos commented Feb 3, 2024

@joweste
Copy link

joweste commented Feb 3, 2024

Please, What's difference between join a group and link a group to community?

@alechkos
Copy link
Collaborator Author

alechkos commented Feb 3, 2024

@joweste

Please, What's difference between join a group and link a group to community?

With joinSubgroup you as a user can become a member of one of the community subgroups (similar to acceptInvite or acceptGroupV4Invite),
with linkSubgroups you as a community admin can link to that community (add) more subgroups

@joweste
Copy link

joweste commented Feb 3, 2024

With joinSubgroup you as a user can become a member of one of the community subgroups (similar to acceptInvite)

Hi, But in your sample, you are joining a group to community, not a user. Is this ok?

const community = await client.getChatById('[email protected]');
result = await community.joinSubgroup('[email protected]');

@alechkos
Copy link
Collaborator Author

alechkos commented Feb 3, 2024

@joweste

Hi, But in your sample, you are joining a group to community, not a user. Is this ok?

In the sample I am not joining a group to community, but the current user (a bot) joins the subgroup of a community

@joweste
Copy link

joweste commented Feb 3, 2024

@joweste

Hi, But in your sample, you are joining a group to community, not a user. Is this ok?

In the sample I am not joining a group to community, but the current user (a bot) joins the subgroup of a community

I got it. Thanks.
This syntax is more clear to me:

/** Joins a community subgroup by community ID and a subgroup ID */
 joinSubgroup: (communityId: string, subGroupId: string) => Promise<JoinGroupResponse>;

@joweste
Copy link

joweste commented Feb 4, 2024

Hi, does a specific subgroup can be linked to many communities?

@saumilsdk
Copy link

Really required feature!! Please approve and merge :)

@joweste
Copy link

joweste commented Feb 24, 2024

@joweste

Sorry, but the community admin just said me, if I am member at least one group of the community, then I am already member of the community.

You are right, my fault. I have to check the Community.getParticipants method, could be that sonething is broke in the latest wweb versions

Hi, is there some news about it? Sorry to ask you.

@reddyram
Copy link

Does group_join and group_ leave with communities as well?
I mean do they get triggered when user leaves or joins a community?

@alechkos alechkos marked this pull request as draft April 16, 2024 02:52
Repository owner locked and limited conversation to collaborators Apr 16, 2024
Repository owner unlocked this conversation Apr 19, 2024
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
Development

Successfully merging this pull request may close these issues.

Trying to remove participants throws an exception Community Group Features
6 participants