A group or clan brings together a bunch of users into a small community or team.
A group is made up of a superadmin, admins, and members. It can be public or private, which determines whether anyone can join the group. Private groups (open=false) are similar to how WhatsApp groups work, a user can only be added when they’re invited to join by one of the group’s admins.
A group also has a maximum member count. This is set to 100 by default if the group is created by the client, or can be overridden if the group is created by the code runtime.
A group user has four states:
Code
Purpose
0
Superadmin
There must at least be 1 superadmin in any group. The superadmin has all the privileges of the admin and can additionally delete the group and promote admin members.
1
Admin
There can be one of more admins. Admins can update groups as well as accept, kick, promote, demote, ban or add members.
2
Member
Regular group member. They cannot accept join requests from new users.
3
Join request
A new join request from a new user. This does not count towards the maximum group member count.
Groups can be listed using a number of optional filters: name, lang_tag, open and (number of) members. If all filters are omitted, the operation will list all existing groups.
The name filter is case insensitive and mutually exclusive to the remainder filters. It can be useful to help the user look for a specific group by name, and it supports the % wildcard for partial matches as a suffix. As an example, looking for a group that is prefixed with the “Persian” word would be written as persian% name filter.
The remainder filters can be combined or omitted in any way, for instance, we could use the open and members filters to list all open groups with at most the specified amount of members.
Client
1
2
constgroups=awaitclient.listGroups(session,"heroes%",20);// fetch first 20 groups
console.info("List of groups:",groups);
Client
1
2
3
4
5
6
7
// Filter for group names which start with "heroes"conststringnameFilter="heroes%";varresult=awaitclient.ListGroupsAsync(session,nameFilter,20);foreach(varginresult.Groups){System.Console.WriteLine("Group name '{0}' count '{1}'",g.Name,g.EdgeCount);}
Client
1
2
3
4
5
6
// Filter for group names which start with "heroes"letnameFilter="heroes%"varresult=tryawaitclient.listGroups(session:session,name:nameFilter,limit:20)forginresult.groups{debugPrint("Group name",g.name,"count",g.edgeCount)}
Client
1
2
3
4
5
6
7
8
9
10
// Filter for group names which start with "heroes"
constnameFilter='heroes%';finalresult=awaitclient.listGroups(session:session,name:nameFilter,limit:20,);for(finalginresult.groups){print('Group name ${g.name}, count ${g.edgeCount}');}
Client
1
2
3
4
5
6
7
8
9
10
11
autosuccessCallback=[this](NGroupListPtrlist){for(auto&group:list->groups){std::cout<<"Group name '"<<group.name<<"' count "<<group.edgeCount<<std::endl;}};// Filter for group names which start with "heroes"
// fetch first 20 groups
client->listGroups(session,"heroes%",20,"",successCallback);
Client
1
2
3
4
5
6
7
// Filter for group names which start with "heroes"StringnameFilter="heroes%";GroupListgroups=client.listGroups(session,nameFilter,20).get();for(Groupgroup:groups.getGroupsList()){System.out.format("Group name %s count %s",group.getName(),group.getEdgeCount());}
Client
1
2
3
4
5
6
7
8
9
varlist:NakamaAPI.ApiGroupList=yield(client.list_groups_async(session,"heroes*",20),"completed")iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])
Client
1
2
3
4
5
6
7
8
9
varlist:NakamaAPI.ApiGroupList=awaitclient.list_groups_async(session,"heroes*",20)iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])
The message response for a list of groups contains a cursor. The cursor can be used to quickly retrieve the next set of results.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/group?limit=20&name=heroes%25&cursor=somecursor"\
-H 'Authorization: Bearer <session token>'
Client
1
2
constgroups=awaitclient.listGroups(session,"heroes%",20,cursor);console.info("List of groups:",groups);
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Filter for group names which start with "heroes"conststringnameFilter="heroes%";varresult=awaitclient.ListGroupsAsync(session,nameFilter,20);// If there are more results get next page.if(result.Cursor!=null){result=awaitclient.ListGroupsAsync(session,nameFilter,20,result.Cursor);foreach(varginresult.Groups){System.Console.WriteLine("Group name '{0}' count '{1}'",g.Name,g.EdgeCount);}}
Client
1
2
3
4
5
6
7
8
9
10
11
12
// Filter for group names which start with "heroes"letnameFilter="heroes%"varresult=tryawaitclient.listGroups(session:session,name:nameFilter,limit:20)// If there are more results get next page.if!result.cursor.isEmpty{result=tryawaitclient.listGroups(session:session,name:nameFilter,limit:20,cursor:result.cursor)forginresult.groups{debugPrint("Group name",g.name,"count",g.edgeCount)}}
Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Filter for group names which start with "heroes"
constnameFilter='heroes%';varresult=awaitclient.listGroups(session:session,name:nameFilter,limit:20,);// If there are more results get next page.
if(result.cursor!=null){result=awaitclient.listGroups(session:session,name:nameFilter,limit:20,cursor:result.cursor);for(finalginresult.groups){print('Group name ${g.name}, count ${g.edgeCount}');}}
voidYourClass::processGroupList(NGroupListPtrlist){for(auto&group:list->groups){std::cout<<"Group name '"<<group.name<<"' count "<<group.edgeCount<<std::endl;}if(!list->cursor.empty()){// request next page
requestHeroes(list->cursor);}}voidYourClass::requestHeroes(conststring&cursor){// Filter for group names which start with "heroes"
// fetch first 20 groups
client->listGroups(session,"heroes%",20,cursor,std::bind(&YourClass::processGroupList,this,std::placeholders::_1));}
Client
1
2
3
4
5
6
7
8
9
10
11
// Filter for group names which start with "heroes"StringnameFilter="heroes%";GroupListgroups=client.listGroups(session,nameFilter,20).get();if(groups.getCursor()!=null){groups=client.listGroups(session,nameFilter,20,groups.getCursor()).get();for(Groupgroup:groups.getGroupsList()){System.out.format("Group name %s count %s",group.getName(),group.getEdgeCount());}}
varlist:NakamaAPI.ApiGroupList=yield(client.list_groups_async(session,"heroes*",20),"completed")iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])varcursor=list.cursorwhilecursor:# While there are more results get next page.list=yield(client.list_groups_async(session,"heroes*",20,cursor),"completed")iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])cursor=list.cursor
varlist:NakamaAPI.ApiGroupList=awaitclient.list_groups_async(session,"heroes*",20)iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])varcursor=list.cursorwhilecursor:# While there are more results get next page.list=awaitclient.list_groups_async(session,"heroes*",20,cursor)iflist.is_exception():print("An error occurred: %s"%list)returnforginlist.groups:vargroup=gasNakamaAPI.ApiGroupprint("Group: name %s, id %s",[group.name,group.id])cursor=list.cursor
When a user has found a group to join they can request to become a member. A public group can be joined without any need for permission while a private group requires a superadmin or an admin to accept the user.
When a user joins or leaves a group event messages are added to chat history. This makes it easy for members to see what’s changed in the group.
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/join"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
constgroup_id="<group id>";awaitclient.joinGroup(session,group_id);console.info("Sent group join request",group_id);
Client
1
2
3
conststringgroupId="<group id>";awaitclient.JoinGroupAsync(session,groupId);System.Console.WriteLine("Sent group join request '{0}'",groupId);
Client
1
2
3
letgroupId="<group id>"tryawaitclient.joinGroup(session:session,groupId:groupId)debugPrint("Sent group join request",groupId)
Client
1
2
3
4
5
6
constgroupId='<group id>';awaitclient.joinGroup(session:session,groupId:groupId,);print('Sent group join request ${groupId}');
Client
1
2
3
4
5
6
7
autosuccessCallback=[](){std::cout<<"Sent group join request"<<std::endl;};stringgroup_id="<group id>";client->joinGroup(session,group_id,successCallback);
Client
1
2
3
Stringgroupid="<group id>";client.joinGroup(session,groupid).get();System.out.format("Sent group join request %s",groupid);
Client
1
2
3
4
5
6
7
8
vargroup_id="<group id>"varjoin:NakamaAsyncResult=yield(client.join_group_async(session,group_id),"completed")ifjoin.is_exception():print("An error occurred: %s"%join)returnprint("Sent group join request %s"%group_id)
Client
1
2
3
4
5
6
7
8
vargroup_id="<group id>"varjoin:NakamaAsyncResult=awaitclient.join_group_async(session,group_id)ifjoin.is_exception():print("An error occurred: %s"%join)returnprint("Sent group join request %s"%group_id)
localgroup_id="<group_id>"localresult=client.join_group(group_id)ifresult.errorthenprint(result.message)returnendprint("Sent group join request",group_id)
The user will receive an in-app notification when they’ve been added to the group. In a private group an admin or superadmin will receive a notification when a user has requested to join.
Each user can list groups they’ve joined as a member or an admin or a superadmin. The list also contains groups which they’ve requested to join but not been accepted into yet.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/user/<user id>/group"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
7
8
constuserId="<user id>";constgroups=awaitclient.listUserGroups(session,userid);groups.user_groups.forEach(function(userGroup){console.log("Group: name '%o' id '%o'.",userGroup.group.name,userGroup.group.id);// group.State is one of: SuperAdmin, Admin, Member, or Join.
console.log("Group's state is %o.",userGroup.state);});
Client
1
2
3
4
5
6
7
8
conststringuserId="<user id>";varresult=awaitclient.ListUserGroupsAsync(session,userId);foreach(varuginresult.UserGroups){varg=ug.Group;System.Console.WriteLine("Group '{0}' role '{1}'",g.Id,ug.State);}
constuserId='<user id>';finalresult=awaitclient.listUserGroups(session:session,userId:userId,);for(finaluginresult.userGroups){print('Group: name ${ug.group.name}, role ${ug.state}');}
Client
1
2
3
4
5
6
7
8
9
10
autosuccessCallback=[](NUserGroupListPtrlist){for(auto&userGroup:list->userGroups){std::cout<<"Group name "<<userGroup.group.name<<std::endl;}};stringuserId="<user id>";client->listUserGroups(session,userId,{},{},{},successCallback);
Client
1
2
3
4
5
6
Stringuserid="<user id>";UserGroupListuserGroups=client.listUserGroups(session,userid).get();for(UserGroupList.UserGroupuserGroup:userGroups.getUserGroupsList()){System.out.format("Group name %s role %d",userGroup.getGroup().getName(),userGroup.getState());}
Client
1
2
3
4
5
6
7
8
9
10
varuser_id="<user id>"varresult:NakamaAPI.ApiUserGroupList=yield(client.list_user_groups_async(session,user_id),"completed")ifresult.is_exception():print("An error occurred: %s"%result)returnforuginresult.user_groups:varg=ug.groupasNakamaAPI.ApiGroupprint("Group %s role %s",g.id,ug.state)
Client
1
2
3
4
5
6
7
8
9
10
varuser_id="<user id>"varresult:NakamaAPI.ApiUserGroupList=awaitclient.list_user_groups_async(session,user_id)ifresult.is_exception():print("An error occurred: %s"%result)returnforuginresult.user_groups:varg=ug.groupasNakamaAPI.ApiGroupprint("Group %s role %s",g.id,ug.state)
A user can list all members who’re part of their group. These include other users who’ve requested to join the private group but not been accepted into yet.
Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/group/<group id>/user"\
-H 'Authorization: Bearer <session token>'
Client
1
2
3
constgroup_id="<group id>";constusers=awaitclient.listGroupUsers(session,group_id);console.info("Users in group:",users);
Client
1
2
3
4
5
6
7
conststringgroupId="<group id>";varresult=awaitclient.ListGroupUsersAsync(session,groupId);foreach(varuginresult.UserGroups){varg=ug.Group;System.Console.WriteLine("group '{0}' role '{1}'",g.Id,ug.State);}
Client
1
2
3
4
5
6
letgroupId="<group id>"letresult=tryawaitclient.listGroupUsers(session:session,groupId:groupId,limit:20)foruginresult.groupUsers{letg=ug.userprint("group '\(g.id)' role '\(ug.state)'")}
Client
1
2
3
4
5
6
7
8
9
constgroupId='<group id>';finalresult=awaitclient.listGroupUsers(session:session,groupId:groupId,);for(finaluginresult.groupUsers){finalg=ug.user;print('group ${g.id} role ${ug.state}');}
Client
1
2
3
4
5
6
7
autosuccessCallback=[](NGroupUserListPtrlist){std::cout<<"Users in group: "<<list->groupUsers<<std::endl;};stringgroup_id="<group id>";client->listGroupUsers(session,group_id,{},{},{},successCallback);
Client
1
2
3
4
5
6
Stringgroupid="<group id>";GroupUserListgroupUsers=client.listGroupUsers(session,groupid).get();for(GroupUserList.GroupUsergroupUser:groupUsers.getGroupUsersList()){System.out.format("Username %s role %d",groupUser.getUser().getUsername(),groupUser.getState());}
Client
1
2
3
4
5
6
7
8
9
10
vargroup_id="<group id>"varmember_list:NakamaAPI.ApiGroupUserList=yield(client.list_group_users_async(session,group_id),"completed")ifmember_list.is_exception():print("An error occurred: %s"%member_list)returnforuginmember_list.group_users:varu=ug.userasNakamaAPI.ApiUserprint("User %s role %s"%[u.id,ug.state])
Client
1
2
3
4
5
6
7
8
9
10
vargroup_id="<group id>"varmember_list:NakamaAPI.ApiGroupUserList=awaitclient.list_group_users_async(session,group_id)ifmember_list.is_exception():print("An error occurred: %s"%member_list)returnforuginmember_list.group_users:varu=ug.userasNakamaAPI.ApiUserprint("User %s role %s"%[u.id,ug.state])
A group can be created with a name and other optional fields. These optional fields are used when a user lists and filter groups. The user who creates the group becomes the owner and a superadmin for it.
You can store additional fields for a group in group.metadata. This is useful to share data you want to be publicly available to users, and providing additional details usable for listing and filtering groups.
Metadata is limited to 16KB per group and can only be set via the script runtime.
The following example shows how you might use group metadata to extend Nakama’s group member permissions with added roles (as described in the best practices guide). Specifically, we will introduce the concept of a bouncer role which a group member must have in order to kick another member from the group.
// Assuming a group metadata structure as follows
constmetadata={roles:{'000d8152-3258-457b-905b-05a9223c5c8c':['bouncer'],'2c0c8e80-fcbc-4b61-901a-dace129f45f5':['bouncer','vip'],}}// Add a before hook to only allow bouncers to kick group users
letBeforeKickGroupUser: nkruntime.BeforeHookFunction<KickGroupUsersRequest>=function(ctx: nkruntime.Context,logger: nkruntime.Logger,nk: nkruntime.Nakama,data: nkruntime.KickGroupUsersRequest):nkruntime.KickGroupUsersRequest|void{constgroups=nk.groupsGetId([data.groupId]);if(groups.length==0){logger.warn('Invalid group Id');returnnull;}// Only continue with the Kick request if the actioning user has the bouncer role
constroles=groups[0].metadata.roles;if(roles&&roles[ctx.userId]&&roles[ctx.userId].includes('bouncer')){returndata;}logger.warn("you must be a bouncer to kick group members")returnnull;};// Register inside InitModule
initializer.registerBeforeKickGroupUsers(BeforeKickGroupUser);
metadata:=map[string]interface{}{"roles":map[string][]string{"000d8152-3258-457b-905b-05a9223c5c8c":{"bouncer"},"2c0c8e80-fcbc-4b61-901a-dace129f45f5":{"bouncer","vip"},},}// Add a before hook to only allow bouncers to kick group users
iferr:=initializer.RegisterBeforeKickGroupUsers(func(ctxcontext.Context,loggerruntime.Logger,db*sql.DB,nkruntime.NakamaModule,in*api.KickGroupUsersRequest)(*api.KickGroupUsersRequest,error){userId,ok:=ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)if!ok{logger.Error("invalid user")returnnil,runtime.NewError("invalid user",13)}groups,err:=nk.GroupsGetId(ctx,[]string{in.GroupId})iferr!=nil||len(groups)==0{logger.Error("group not found")returnnil,runtime.NewError("group not found",5)}// Only continue with the Kick request if the actioning user has the bouncer role
varmetadatamap[string]interface{}iferr:=json.Unmarshal([]byte(groups[0].GetMetadata()),&metadata);err!=nil{logger.Error("error deserializing metadata")returnnil,runtime.NewError("error deserializing metadata",13)}for_,role:=rangemetadata.Roles[userId]{ifrole=="bouncer"{returnin,nil}}returnnil,runtime.NewError("you must be a bouncer to kick group members",7)});err!=nil{logger.Error("unable to register before kick group users hook: %v",err)returnerr}
-- Assuming a group metadata structure as followslocalmetadata={["roles"]={["000d8152-3258-457b-905b-05a9223c5c8c"]={"bouncer"},["2c0c8e80-fcbc-4b61-901a-dace129f45f5"]={"bouncer","vip"},}}-- Add a before hook to only allow bouncers to kick group userslocalfunctionbefore_kick_group_users(context,payload)localgroups=nk.groups_get_id({payload.group_id})if#groups==0thennk.logger_error("group not found")returnnilendlocalroles=groups[1].metadata["roles"]ifroles==nilorroles[context.user_id]==nilthennk.logger_error("no roles configured for user")returnnilendfor_,roleinipairs(roles[context.user_id])doifrole=="bouncer"thenreturnpayloadendendnk.logger_error("you must be a bouncer to kick group members")returnnilendnk.register_req_before(before_kick_group_users,"KickGroupUsers")
When a group has been created it’s admins can update optional fields.
Client
1
2
3
4
5
curl -X PUT "http://127.0.0.1:7350/v2/group/<group id>"\
-H 'Authorization: Bearer <session token>'\
-d '{
"description": "Better than Marvel Heroes!",
}'
Client
1
2
3
4
constgroup_id="<group id>";constdescription="Better than Marvel Heroes!";constgroup=awaitclient.updateGroup(session,group_id,{description:description});console.info("Updated group:",group);
Client
1
2
3
4
conststringgroupId="<group id>";conststringdesc="Better than Marvel Heroes!";vargroup=awaitclient.UpdateGroupAsync(session,groupId,null,desc);System.Console.WriteLine("Updated group: {0}",group);
Client
1
2
3
4
letgroupId="<group id>"letdescription="Better than Marvel Heroes!"tryawaitclient.updateGroup(session:session,groupId:groupId,description:description)debugPrint("Updated group:",groupId)
Client
1
2
3
4
5
6
7
8
constgroupId='<group id>';constdescription='Better than Marvel Heroes!';awaitclient.updateGroup(session:session,groupId:groupId,description:description,);print('Updated group: ${groupId}');
Client
1
2
3
4
5
6
7
8
autosuccessCallback=[](){std::cout<<"Updated group"<<std::endl;};stringgroup_id="<group id>";stringdescription="Better than Marvel Heroes!";client->updateGroup(session,group_id,opt::nullopt,description,opt::nullopt,opt::nullopt,opt::nullopt,successCallback);
Client
1
2
3
4
Stringgroupid="<group id>";Stringdesc="Better than Marvel Heroes!";client.updateGroup(session,groupid,null,desc).get();System.out.format("Updated group %s",groupid);
Client
1
2
3
4
5
6
7
8
9
vargroup_id="<group id>"vardescription="Better than Marvel Heroes!"varupdate:NakamaAsyncResult=yield(client.update_group_async(session,group_id,null,description),"completed")ifupdate.is_exception():print("An error occurred: %s"%update)returnprint("Updated group")
Client
1
2
3
4
5
6
7
8
9
vargroup_id="<group id>"vardescription="Better than Marvel Heroes!"varupdate:NakamaAsyncResult=awaitclient.update_group_async(session,group_id,null,description)ifupdate.is_exception():print("An error occurred: %s"%update)returnprint("Updated group")
Client
1
2
3
4
5
6
7
8
PUT /v2/group/<group id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{"description": "I was only kidding. Basil sauce ftw!",
}
Updating a group’s size is an action that can only be performed authoritatively via the server.
Server
1
2
3
4
5
6
7
8
localnk=require("nakama")localnew_max_size=50localsuccess,err=pcall(nk.group_update("<GroupId>",nil,"","","","","",nil,nil,new_max_size))if(notsuccess)thennk.logger_error(("Could not update group: %q"):format(err))end
A user can leave a group and will no longer be able to join group chat or read message history. If the user is a superadmin they will only be able to leave when at least one other superadmin exists in the group.
Any user who leaves the group will generate an event message in group chat which other members can read.
Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/leave"\
-H 'Authorization: Bearer <session token>'
Each group is managed by one or more superadmins or admins. These users are members with permission to make changes to optional fields, accept or reject new members, remove members or other admins, and promote other members as admins.
A group must have at least one superadmin. The last superadmin has to promote another member before they can leave.
When a user joins a private group it will create a join request until an admin accepts or rejects the user. The superadmin or admin can accept the user into the group.
autosuccessCallback=[](){std::cout<<"added user to group"<<std::endl;};stringgroup_id="<group id>";stringuser_id="<user id>";client->addGroupUsers(session,group_id,{user_id},successCallback);
The user will receive an in-app notification when they’ve been added to the group. In a private group an admin will receive a notification about the join request.
To reject the user from joining the group you should kick them.
An admin can promote another member of the group as an admin. This grants the member the same privileges to manage the group. A group can have one or more admins.
autosuccessCallback=[](){std::cout<<"user has been promoted"<<std::endl;};stringgroup_id="<group id>";stringuser_id="<user id>";client->promoteGroupUsers(session,group_id,{user_id},successCallback);
An admin can demote another member of the group down a role. This revokes the member of his current privileges to and assigns member the privileges available in the demoted role. Members who are already at the lowest role in their group will not be affected by a demotion.
Client
1
2
3
4
5
6
7
8
autosuccessCallback=[](){std::cout<<"user has been demoted"<<std::endl;};stringgroup_id="<group id>";stringuser_id="<user id>";client->demoteGroupUsers(session,group_id,{user_id},successCallback);
An admin or superadmin can kick a member from the group. The user is removed but can re-join again later unless the user is banned or the group is private in which case an admin must accept the re-join request.
If a user is removed from a group it does not prevent them from joining other groups. Sometimes a bad user needs to be kicked from the group and banned from re-joining either the group or the whole server. This will prevent the user from being able to connect to the server and interact at all.
autosuccessCallback=[](){std::cout<<"user has been kicked"<<std::endl;};stringgroup_id="<group id>";stringuser_id="<user id>";client->kickGroupUsers(session,group_id,{user_id},successCallback);
An admin or superadmin can ban a member from the group. The user is kicked from the group and prevented from re-joining or even requesting to re-join.
The user can be unbanned either via the Nakama Console or a runtime code function.
Client
1
2
3
4
5
6
7
8
autosuccessCallback=[](){std::cout<<"user has been banned"<<std::endl;};stringgroup_id="<group id>";stringuser_id="<user id>";client->banGroupUsers(session,group_id,{user_id},successCallback);
A group can only be removed by one of the superadmins which will disband all members. When a group is removed it’s name can be re-used to create a new group.