Groups #

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:

CodePurpose
0SuperadminThere 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.
1AdminThere can be one of more admins. Admins can update groups as well as accept, kick, promote, demote, ban or add members.
2MemberRegular group member. They cannot accept join requests from new users.
3Join requestA new join request from a new user. This does not count towards the maximum group member count.

List and filter groups #

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
const groups = await client.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"
const string nameFilter = "heroes%";
var result = await client.ListGroupsAsync(session, nameFilter, 20);
foreach (var g in result.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"
let nameFilter = "heroes%"
var result = try await client.listGroups(session: session, name: nameFilter, limit: 20)
for g in result.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"
const nameFilter = 'heroes%';
final result = await client.listGroups(
  session: session,
  name: nameFilter,
  limit: 20,
);
for (final g in result.groups) {
  print('Group name ${g.name}, count ${g.edgeCount}');
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = [this](NGroupListPtr list)
{
    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"
String nameFilter = "heroes%";
GroupList groups = client.listGroups(session, nameFilter, 20).get();

for (Group group : groups.getGroupsList()) {
    System.out.format("Group name %s count %s", group.getName(), group.getEdgeCount());
}
Client
1
2
3
4
5
6
7
8
9
var list : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, "heroes*", 20), "completed")

if list.is_exception():
    print("An error occurred: %s" % list)
    return

for g in list.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name %s, id %s", [group.name, group.id])
Client
1
2
3
4
5
6
7
8
9
var list : NakamaAPI.ApiGroupList = await client.list_groups_async(session, "heroes*", 20)

if list.is_exception():
    print("An error occurred: %s" % list)
    return

for g in list.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name %s, id %s", [group.name, group.id])
Client
1
2
curl "http://127.0.0.1:7350/v2/group?limit=20&name=heroes%25" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
GET /v2/group?limit=20&name=heroes%&cursor=<cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
local result = client.list_groups("heroes*", "<cursor>", 20)

for _,group in ipairs(result.groups) do
  pprint(groups)
end

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
const groups = await client.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"
const string nameFilter = "heroes%";
var result = await client.ListGroupsAsync(session, nameFilter, 20);

// If there are more results get next page.
if (result.Cursor != null)
{
    result = await client.ListGroupsAsync(session, nameFilter, 20, result.Cursor);

    foreach (var g in result.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"
let nameFilter = "heroes%"
var result = try await client.listGroups(session: session, name: nameFilter, limit: 20)

// If there are more results get next page.
if !result.cursor.isEmpty {
    result = try await client.listGroups(session: session, name: nameFilter, limit: 20, cursor: result.cursor)

    for g in result.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"
const nameFilter = 'heroes%';
var result = await client.listGroups(
  session: session,
  name: nameFilter,
  limit: 20,
);

// If there are more results get next page.
if (result.cursor != null) {
  result = await client.listGroups(session: session, name: nameFilter, limit: 20, cursor: result.cursor);

  for (final g in result.groups) {
    print('Group name ${g.name}, count ${g.edgeCount}');
  }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void YourClass::processGroupList(NGroupListPtr list)
{
    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);
    }
}

void YourClass::requestHeroes(const string& 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"
String nameFilter = "heroes%";
GroupList groups = client.listGroups(session, nameFilter, 20).get();

if (groups.getCursor() != null) {
    groups = client.listGroups(session, nameFilter, 20, groups.getCursor()).get();

    for (Group group : groups.getGroupsList()) {
        System.out.format("Group name %s count %s", group.getName(), group.getEdgeCount());
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var list : NakamaAPI.ApiGroupList = yield(client.list_groups_async(session, "heroes*", 20), "completed")

if list.is_exception():
    print("An error occurred: %s" % list)
    return

for g in list.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name %s, id %s", [group.name, group.id])

var cursor = list.cursor

while cursor: # While there are more results get next page.
    list = yield(client.list_groups_async(session, "heroes*", 20, cursor), "completed")

    if list.is_exception():
        print("An error occurred: %s" % list)
        return

    for g in list.groups:
        var group = g as NakamaAPI.ApiGroup
        print("Group: name %s, id %s", [group.name, group.id])

    cursor = list.cursor
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var list : NakamaAPI.ApiGroupList = await client.list_groups_async(session, "heroes*", 20)

if list.is_exception():
    print("An error occurred: %s" % list)
    return

for g in list.groups:
    var group = g as NakamaAPI.ApiGroup
    print("Group: name %s, id %s", [group.name, group.id])

var cursor = list.cursor

while cursor: # While there are more results get next page.
    list = await client.list_groups_async(session, "heroes*", 20, cursor)

    if list.is_exception():
        print("An error occurred: %s" % list)
        return

    for g in list.groups:
        var group = g as NakamaAPI.ApiGroup
        print("Group: name %s, id %s", [group.name, group.id])

    cursor = list.cursor
Client
1
2
3
4
5
GET /v2/group?limit=20&name=heroes%&cursor=somecursor
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local cursor = nil

repeat
  local result = client.list_groups("heroes*", cursor, 20)

  if result.error then
    print(result.message)
    break
  end

  cursor = result.cursor

  for _,group in ipairs(result.groups) do
    pprint(groups)
  end
until not cursor

Join groups #

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.

A user who’s part of a group can join group chat and access it’s message history.

Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/join" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
await client.joinGroup(session, group_id);
console.info("Sent group join request", group_id);
Client
1
2
3
const string groupId = "<group id>";
await client.JoinGroupAsync(session, groupId);
System.Console.WriteLine("Sent group join request '{0}'", groupId);
Client
1
2
3
let groupId = "<group id>"
try await client.joinGroup(session: session, groupId: groupId)
debugPrint("Sent group join request", groupId)
Client
1
2
3
4
5
6
const groupId = '<group id>';
await client.joinGroup(
  session: session,
  groupId: groupId,
);
print('Sent group join request ${groupId}');
Client
1
2
3
4
5
6
7
auto successCallback = []()
{
    std::cout << "Sent group join request" << std::endl;
};

string group_id = "<group id>";
client->joinGroup(session, group_id, successCallback);
Client
1
2
3
String groupid = "<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
var group_id = "<group id>"
var join : NakamaAsyncResult = yield(client.join_group_async(session, group_id), "completed")

if join.is_exception():
    print("An error occurred: %s" % join)
    return

print("Sent group join request %s" % group_id)
Client
1
2
3
4
5
6
7
8
var group_id = "<group id>"
var join : NakamaAsyncResult = await client.join_group_async(session, group_id)

if join.is_exception():
    print("An error occurred: %s" % join)
    return

print("Sent group join request %s" % group_id)
Client
1
2
3
4
5
POST /v2/group/<group id>/join
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
8
9
local group_id = "<group_id>"
local result = client.join_group(group_id)

if result.error then
  print(result.message)
  return
end

print("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.

List a user’s groups #

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
const userId = "<user id>";
const groups = await client.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
const string userId = "<user id>";
var result = await client.ListUserGroupsAsync(session, userId);

foreach (var ug in result.UserGroups)
{
    var g = ug.Group;
    System.Console.WriteLine("Group '{0}' role '{1}'", g.Id, ug.State);
}
Client
1
2
3
4
5
6
let userId = "<user id>"
var result = try await client.listUserGroups(session: session, userId: userId)

for ug in result.userGroups {
    debugPrint("Group: name", ug.group.name, "role", ug.state)
}
Client
1
2
3
4
5
6
7
8
9
const userId = '<user id>';
final result = await client.listUserGroups(
  session: session,
  userId: userId,
);

for (final ug in result.userGroups) {
  print('Group: name ${ug.group.name}, role ${ug.state}');
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto successCallback = [](NUserGroupListPtr list)
{
    for (auto& userGroup : list->userGroups)
    {
        std::cout << "Group name " << userGroup.group.name << std::endl;
    }
};

string userId = "<user id>";
client->listUserGroups(session, userId, {}, {}, {}, successCallback);
Client
1
2
3
4
5
6
String userid = "<user id>";
UserGroupList userGroups = client.listUserGroups(session, userid).get();

for (UserGroupList.UserGroup userGroup : 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
var user_id = "<user id>"
var result : NakamaAPI.ApiUserGroupList = yield(client.list_user_groups_async(session, user_id), "completed")

if result.is_exception():
    print("An error occurred: %s" % result)
    return

for ug in result.user_groups:
    var g = ug.group as NakamaAPI.ApiGroup
    print("Group %s role %s", g.id, ug.state)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var user_id = "<user id>"
var result : NakamaAPI.ApiUserGroupList = await client.list_user_groups_async(session, user_id)

if result.is_exception():
    print("An error occurred: %s" % result)
    return

for ug in result.user_groups:
    var g = ug.group as NakamaAPI.ApiGroup
    print("Group %s role %s", g.id, ug.state)
Client
1
2
3
4
5
GET /v2/user/<user id>/group
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
local user_id = "<user>"
local result = client.list_user_groups(user_id)

if result.error then
  print(result.message)
  return
end

for _,user_group in ipairs(result.user_groups) do
  pprint(user_group)
end

List group members #

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
const group_id = "<group id>";
const users = await client.listGroupUsers(session, group_id);
console.info("Users in group:", users);
Client
1
2
3
4
5
6
7
const string groupId = "<group id>";
var result = await client.ListGroupUsersAsync(session, groupId);
foreach (var ug in result.UserGroups)
{
    var g = ug.Group;
    System.Console.WriteLine("group '{0}' role '{1}'", g.Id, ug.State);
}
Client
1
2
3
4
5
6
let groupId = "<group id>"
let result = try await client.listGroupUsers(session: session, groupId: groupId, limit: 20)
for ug in result.groupUsers {
    let g = ug.user
    print("group '\(g.id)' role '\(ug.state)'")
}
Client
1
2
3
4
5
6
7
8
9
const groupId = '<group id>';
final result = await client.listGroupUsers(
  session: session,
  groupId: groupId,
);
for (final ug in result.groupUsers) {
  final g = ug.user;
  print('group ${g.id} role ${ug.state}');
}
Client
1
2
3
4
5
6
7
auto successCallback = [](NGroupUserListPtr list)
{
    std::cout << "Users in group: " << list->groupUsers << std::endl;
};

string group_id = "<group id>";
client->listGroupUsers(session, group_id, {}, {}, {}, successCallback);
Client
1
2
3
4
5
6
String groupid = "<group id>";
GroupUserList groupUsers = client.listGroupUsers(session, groupid).get();

for (GroupUserList.GroupUser groupUser : 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
var group_id = "<group id>"
var member_list : NakamaAPI.ApiGroupUserList = yield(client.list_group_users_async(session, group_id), "completed")

if member_list.is_exception():
    print("An error occurred: %s" % member_list)
    return

for ug in member_list.group_users:
    var u = ug.user as NakamaAPI.ApiUser
    print("User %s role %s" % [u.id, ug.state])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var group_id = "<group id>"
var member_list : NakamaAPI.ApiGroupUserList = await client.list_group_users_async(session, group_id)

if member_list.is_exception():
    print("An error occurred: %s" % member_list)
    return

for ug in member_list.group_users:
    var u = ug.user as NakamaAPI.ApiUser
    print("User %s role %s" % [u.id, ug.state])
Client
1
2
3
4
5
GET /v2/group/<group id>/user
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
local group_id = "<group_id>"
local result = client.list_group_users(group_id)

if result.error then
  print(result.message)
  return
end

for _,user in ipairs(result.group_users) do
  pprint(user)
end

Create a group #

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.

Client
1
2
3
4
5
6
7
8
curl -X POST "http://127.0.0.1:7350/v2/group" \
  -H 'Authorization: Bearer <session token>' \
  -d '{
    "name": "pizza-lovers",
    "description": "pizza lovers, pineapple haters",
    "lang_tag": "en_US",
    "open": true
  }'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const group_name = "pizza-lovers";
const description = "pizza lovers, pineapple haters";

const group = await client.createGroup(session, {
  name: group_name,
  description: description,
  lang_tag: "en_US",
  open: true
});

console.info("New group:", group);
Client
1
2
3
4
const string name = "pizza-lovers";
const string desc = "pizza lovers, pineapple haters";
var group = await client.CreateGroupAsync(session, name, desc);
System.Console.WriteLine("New group: {0}", group);
Client
1
2
3
4
let name = "pizza-lovers"
let desc = "pizza lovers, pineapple haters"
let group = try await client.createGroup(session: session, name: name, description: desc)
debugPrint("New group:", group)
Client
1
2
3
4
5
6
7
8
const name = 'pizza-lovers';
const desc = 'pizza lovers, pineapple haters';
final group = await client.createGroup(
  session: session,
  name: name,
  description: desc,
);
print('New group: ${group}');
Client
1
2
3
4
5
6
7
8
auto successCallback = [](const NGroup& group)
{
    std::cout << "New group ID: " << group.id << std::endl;
};

string group_name = "pizza-lovers";
string description = "pizza lovers, pineapple haters";
client->createGroup(session, group_name, description, "", "en_US", true, {}, successCallback);
Client
1
2
3
4
String name = "pizza-lovers";
String desc = "pizza lovers, pineapple haters";
Group group = client.createGroup(session, name, desc).get();
System.out.format("New group %s", group.getId());
Client
1
2
3
4
5
6
7
8
9
var group_name = "pizza-lovers"
var group_desc = "pizza lovers, pineapple haters"
var group : NakamaAPI.ApiGroup = yield(client.create_group_async(session, group_name, group_desc), "completed")

if group.is_exception():
    print("An error occurred: %s" % group)
    return

print("New group: %s" % group)
Client
1
2
3
4
5
6
7
8
9
var group_name = "pizza-lovers"
var group_desc = "pizza lovers, pineapple haters"
var group : NakamaAPI.ApiGroup = await client.create_group_async(session, group_name, group_desc)

if group.is_exception():
    print("An error occurred: %s" % group)
    return

print("New group: %s" % group)
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /v2/group
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "name": "pizza-lovers",
  "description": "pizza lovers, pineapple haters",
  "lang_tag": "en_US",
  "open": true
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
local avatar_url = ""
local description = "pizza lovers, pineapple haters"
local lang_tag = "en-gb"
local max_count = 10
local name = "pizza-lovers"
local open = true

local result = client.create_group(avatar_url, description, lang_tag, max_count, name, open)

if result.error then
  print(result.message)
  return
end

You can also create a group with server-side code. This can be useful when the group must be created together with some other record or feature.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
local nk = require("nakama")

local metadata = { -- Add whatever custom fields you want.
  my_custom_field = "some value"
}

local user_id = "<user id>"
local name = "pizza-lovers"
local creator_id = user_id
local lang = "en_US"
local description = "pizza lovers, pineapple haters"
local avatar_url = "url://somelink"
local open = true
local maxMemberCount = 100

local success, err = pcall(nk.group_create, user_id, name, creator_id, lang, description, avatar_url, open, metadata, maxMemberCount)

if (not success) then
  nk.logger_error(("Could not create group: %q"):format(err))
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
metadata := map[string]interface{}{
    "my_custom_field": "some value", // Add whatever custom fields you want.
}

userID := "<user id>"
creatorID := userID
name := "pizza-lovers"
description := "pizza lovers, pineapple haters"
langTag := "en"
open := true
avatarURL := "url://somelink"
maxCount := 100

if _, err := nk.GroupCreate(ctx, userID, name, creatorID, langTag, description, avatarURL, open, metadata, maxCount); err != nil {
    logger.Error("Could not create group: %s", err.Error())
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let userId = 'dcb891ea-a311-4681-9213-6741351c9994';
let creatorId = 'dcb891ea-a311-4681-9213-6741351c9994';
let name = 'Some unique group name';
let description = 'My awesome group.';
let lang = 'en';
let open = true;
let avatarURL = 'url://somelink';
let metadata = { custom_field: 'some_value' };
let maxCount = 100;

let group = {} as nkruntime.Group;

try {
    group = nk.groupCreate(userId, name, creatorId, lang, description, avatarURL, open, metadata, maxCount);
} catch (error) {
    // Handle error
}

Group metadata #

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 can also be used to create new functionality on top of existing groups features.

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.

Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Assuming a group metadata structure as follows
const metadata = {
  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
let BeforeKickGroupUser: nkruntime.BeforeHookFunction<KickGroupUsersRequest> = function (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, data: nkruntime.KickGroupUsersRequest): nkruntime.KickGroupUsersRequest | void {
  const groups = nk.groupsGetId([data.groupId]);
  if (groups.length == 0) {
    logger.warn('Invalid group Id');
    return null;
  }
  
  // Only continue with the Kick request if the actioning user has the bouncer role
  const roles = groups[0].metadata.roles;
  if (roles && roles[ctx.userId] && roles[ctx.userId].includes('bouncer')) {
    return data;
  }

  logger.warn("you must be a bouncer to kick group members")
  return null;
};

// Register inside InitModule
initializer.registerBeforeKickGroupUsers(BeforeKickGroupUser);
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
if err := initializer.RegisterBeforeKickGroupUsers(func (ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, in *api.KickGroupUsersRequest) (*api.KickGroupUsersRequest, error) {
  userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string)
  if !ok {
    logger.Error("invalid user")
    return nil, runtime.NewError("invalid user", 13)
  }

  groups, err := nk.GroupsGetId(ctx, []string { in.GroupId })
  if err != nil || len(groups) == 0 {
    logger.Error("group not found")
    return nil, runtime.NewError("group not found", 5)
  }

  // Only continue with the Kick request if the actioning user has the bouncer role
  var metadata map[string]interface{}
  if err := json.Unmarshal([]byte(groups[0].GetMetadata()), &metadata); err != nil {
    logger.Error("error deserializing metadata")
    return nil, runtime.NewError("error deserializing metadata", 13)
  }

  for _, role := range metadata.Roles[userId] {
    if role == "bouncer" {
      return in, nil
    }
  }

  return nil, 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)
  return err
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
-- Assuming a group metadata structure as follows
local metadata = {
  ["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
local function before_kick_group_users(context, payload)
    local groups = nk.groups_get_id({ payload.group_id })
    if #groups == 0 then
        nk.logger_error("group not found")
        return nil
    end

    local roles = groups[1].metadata["roles"]
    if roles == nil or roles[context.user_id] == nil then
        nk.logger_error("no roles configured for user")
        return nil
    end

    for _, role in ipairs(roles[context.user_id]) do
        if role == "bouncer" then
            return payload
        end
    end

    nk.logger_error("you must be a bouncer to kick group members")
    return nil
end

nk.register_req_before(before_kick_group_users, "KickGroupUsers")

Update a group #

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
const group_id = "<group id>";
const description = "Better than Marvel Heroes!";
const group = await client.updateGroup(session, group_id, { description: description });
console.info("Updated group:", group);
Client
1
2
3
4
const string groupId = "<group id>";
const string desc = "Better than Marvel Heroes!";
var group = await client.UpdateGroupAsync(session, groupId, null, desc);
System.Console.WriteLine("Updated group: {0}", group);
Client
1
2
3
4
let groupId = "<group id>"
let description = "Better than Marvel Heroes!"
try await client.updateGroup(session: session, groupId: groupId, description: description)
debugPrint("Updated group:", groupId)
Client
1
2
3
4
5
6
7
8
const groupId = '<group id>';
const description = 'Better than Marvel Heroes!';
await client.updateGroup(
  session: session,
  groupId: groupId,
  description: description,
);
print('Updated group: ${groupId}');
Client
1
2
3
4
5
6
7
8
auto successCallback = []()
{
    std::cout << "Updated group" << std::endl;
};

string group_id = "<group id>";
string description = "Better than Marvel Heroes!";
client->updateGroup(session, group_id, opt::nullopt, description, opt::nullopt, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
String groupid = "<group id>";
String desc = "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
var group_id = "<group id>"
var description = "Better than Marvel Heroes!"
var update : NakamaAsyncResult = yield(client.update_group_async(session, group_id, null, description), "completed")

if update.is_exception():
    print("An error occurred: %s" % update)
    return

print("Updated group")
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var description = "Better than Marvel Heroes!"
var update : NakamaAsyncResult = await client.update_group_async(session, group_id, null, description)

if update.is_exception():
    print("An error occurred: %s" % update)
    return

print("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!",
}
Client
1
2
3
4
5
6
7
8
local group_id = "<group id>"
local avatar_url = "http://example.com/example.jpg"

local result = client.update_group(group_id, avatar_url)

if result.error then
  print(result.message)
end

Updating group size #

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
local nk = require("nakama")

local new_max_size = 50
local success, err = pcall(nk.group_update("<GroupId>", nil, "", "", "", "", "", nil, nil, new_max_size))

if (not success) then
  nk.logger_error(("Could not update group: %q"):format(err))
end
Server
1
2
3
4
5
6
newMaxSize := 50
open := true

if err := nk.GroupUpdate(ctx, "<GroupId>", "", "", "", "", "", open, nil, newMaxSize); err != nil {
    return err
}
Server
1
2
let newMaxSize = 50
nk.groupUpdate("<GroupId>", "", null, null, null, null, null, null, null, newMaxSize);

Leave a group #

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>'
Client
1
2
const group_id = "<group id>";
await client.leaveGroup(session, group_id);
Client
1
2
const string groupId = "<group id>";
await client.LeaveGroupAsync(session, groupId);
Client
1
2
let groupId = "<group id>"
try await client.leaveGroup(session: session, groupId: groupId)
Client
1
2
3
4
5
const groupId = '<group id>';
await client.leaveGroup(
  session: session,
  groupId: groupId,
);
Client
1
2
string groupId = "<group id>";
client->leaveGroup(session, groupId);
Client
1
2
String groupId = "<group id>";
client.leaveGroup(session, groupId).get();
Client
1
2
3
4
5
6
7
8
var group_id = "<group id>"
var leave : NakamaAsyncResult = yield(client.leave_group_async(session, group_id), "completed")

if leave.is_exception():
    print("An error occurred: %s" % leave)
    return

print("Group left")
Client
1
2
3
4
5
6
7
8
var group_id = "<group id>"
var leave : NakamaAsyncResult = await client.leave_group_async(session, group_id)

if leave.is_exception():
    print("An error occurred: %s" % leave)
    return

print("Group left")
Client
1
2
3
4
5
POST /v2/group/<group id>/leave
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
local group_id = "<group id>"
local result = client.leave_group(group_id)

if result.error then
  print(result.message)
end

Manage groups #

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.

Accept new members #

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.

Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/add?user_ids=<user id>" \
    -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
const user_id = "<user id>";
await client.addGroupUsers(session, group_id, [user_id]);
Client
1
2
3
const string groupId = "<group id>";
var userIds = new[] {"<user id>"};
await client.AddGroupUsersAsync(session, groupId, userIds);
Client
1
2
3
let groupId = "<group id>"
let userIds = ["<user id>"]
try await client.addGroupUsers(session: session, groupId: groupId, ids: userIds)
Client
1
2
3
4
5
6
7
const groupId = '<group id>';
final userIds = ['<user id>'];
await client.addGroupUsers(
  session: session,
  groupId: groupId,
  userIds: userIds,
);
Client
1
2
3
4
5
6
7
8
auto successCallback = []()
{
    std::cout << "added user to group" << std::endl;
};

string group_id = "<group id>";
string user_id = "<user id>";
client->addGroupUsers(session, group_id, { user_id }, successCallback);
Client
1
2
3
String groupid = "<group id>";
String[] userIds = new String[] {"<user id>"};
client.addGroupUsers(session, groupid, userIds).get();
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var accept : NakamaAsyncResult = yield(client.add_group_users_async(session, group_id, user_ids), "completed")

if accept.is_exception():
    print("An error occurred: %s" % accept)
    return

print("User added")
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var accept : NakamaAsyncResult = await client.add_group_users_async(session, group_id, user_ids)

if accept.is_exception():
    print("An error occurred: %s" % accept)
    return

print("User added")
Client
1
2
3
4
5
POST /v2/group/<group id>/add?user_ids=<user id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local group_id = "<group id>"
local user_ids = { "<user id>" }
local result = client.add_group_users(group_id, user_ids)

if result.error then
  print(result.message)
end

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.

Promote a member #

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.

Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/promote?user_ids=<user id>" \
    -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
const user_id = "<user id>";
await client.promoteGroupUsers(session, group_id, [user_id]);
Client
1
2
3
const string groupId = "<group id>";
var userIds = new[] {"<user id>"};
await client.PromoteGroupUsersAsync(session, groupId, userIds);
Client
1
2
3
let groupId = "<group id>"
let userIds = ["<user id>"]
try await client.promoteGroupUsers(session: session, groupId: groupId, ids: userIds)
Client
1
2
3
4
5
6
7
const groupId = '<group id>';
final userIds = ['<user id>'];
await client.promoteGroupUsers(
  session: session,
  groupId: groupId,
  userIds: userIds,
);
Client
1
2
3
4
5
6
7
8
auto successCallback = []()
{
    std::cout << "user has been promoted" << std::endl;
};

string group_id = "<group id>";
string user_id = "<user id>";
client->promoteGroupUsers(session, group_id, { user_id }, successCallback);
Client
1
2
3
String groupid = "<group id>";
String[] userIds = new String[] {"<user id>"};
client.promoteGroupUsers(session, groupid, userIds).get();
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var promote : NakamaAsyncResult = yield(client.promote_group_users_async(session, group_id, user_ids), "completed")

if promote.is_exception():
    print("An error occurred: %s" % promote)
    return

print("User promoted")
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var promote : NakamaAsyncResult = await client.promote_group_users_async(session, group_id, user_ids)

if promote.is_exception():
    print("An error occurred: %s" % promote)
    return

print("User promoted")
Client
1
2
3
4
5
POST /v2/group/<group id>/promote?user_ids=<user id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local group_id = "<group id>"
local user_ids = { "<user id>" }
local result = client.promote_group_users(group_id, user_ids)

if result.error then
  print(result.message)
end

Demote a member #

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
auto successCallback = []()
{
    std::cout << "user has been demoted" << std::endl;
};

string group_id = "<group id>";
string user_id = "<user id>";
client->demoteGroupUsers(session, group_id, { user_id }, successCallback);
Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/demote?user_ids=<user id>" \
    -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
const user_id = "<user id>";
await client.demoteGroupUsers(session, group_id, [user_id]);
Client
1
2
3
const string groupId = "<group id>";
var userIds = new[] {"<user id>"};
await client.DemoteGroupUsersAsync(session, groupId, userIds);
Client
1
2
3
let groupId = "<group id>"
let userIds = ["<user id>"]
try await client.demoteGroupUsers(session: session, groupId: groupId, ids: userIds)
Client
1
2
3
4
5
6
7
const groupId = '<group id>';
final userIds = ['<user id>'];
await client.demoteGroupUsers(
  session: session,
  groupId: groupId,
  userIds: userIds,
);
Client
1
2
3
String groupid = "<group id>";
String[] userIds = new String[] {"<user id>"};
client.demoteGroupUsers(session, groupid, userIds).get();
Client
1
2
3
4
5
POST /v2/group/<group id>/demote?user_ids=<user id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local group_id = "<group id>"
local user_ids = { "<user id>" }
local result = client.demote_group_users(group_id, user_ids)

if result.error then
  print(result.message)
end

Kick a member #

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.

Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/kick?user_ids=<user id>" \
    -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
const user_id = "<user id>";
await client.kickGroupUsers(session, group_id, [user_id]);
Client
1
2
3
const string groupId = "<group id>";
var userIds = new[] {"<user id>"};
await client.KickGroupUsersAsync(session, groupId, userIds);
Client
1
2
3
let groupId = "<group id>"
let userIds = ["<user id>"]
try await client.kickGroupUsers(session: session, groupId: groupId, ids: userIds)
Client
1
2
3
4
5
6
7
const groupId = '<group id>';
final userIds = ['<user id>'];
await client.kickGroupUsers(
  session: session,
  groupId: groupId,
  userIds: userIds,
);
Client
1
2
3
4
5
6
7
8
auto successCallback = []()
{
    std::cout << "user has been kicked" << std::endl;
};

string group_id = "<group id>";
string user_id = "<user id>";
client->kickGroupUsers(session, group_id, { user_id }, successCallback);
Client
1
2
3
String groupid = "<group id>";
String[] userIds = new String[] {"<user id>"};
client.kickGroupUsers(session, groupid, userIds).get();
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var kick : NakamaAsyncResult = yield(client.kick_group_users_async(session, group_id, user_ids), "completed")

if kick.is_exception():
    print("An error occurred: %s" % kick)
    return

print("User kicked")
Client
1
2
3
4
5
6
7
8
9
var group_id = "<group id>"
var user_ids = ["<user id>"]
var kick : NakamaAsyncResult = await client.kick_group_users_async(session, group_id, user_ids)

if kick.is_exception():
    print("An error occurred: %s" % kick)
    return

print("User kicked")
Client
1
2
3
4
5
POST /v2/group/<group id>/kick?user_ids=<user id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local group_id = "<group id>"
local user_ids = { "<user id>" }
local result = client.kick_group_users(group_id, user_ids)

if result.error then
  print(result.message)
end

Ban a group member #

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
auto successCallback = []()
{
    std::cout << "user has been banned" << std::endl;
};

string group_id = "<group id>";
string user_id = "<user id>";
client->banGroupUsers(session, group_id, { user_id }, successCallback);
Client
1
2
curl -X POST "http://127.0.0.1:7350/v2/group/<group id>/ban?user_ids=<user id>" \
    -H 'Authorization: Bearer <session token>'
Client
1
2
3
const group_id = "<group id>";
const user_id = "<user id>";
await client.banGroupUsers(session, group_id, [user_id]);
Client
1
2
3
const string groupId = "<group id>";
var userIds = new[] {"<user id>"};
await client.BanGroupUsersAsync(session, groupId, userIds);
Client
1
2
3
let groupId = "<group id>"
let userIds = ["<user id>"]
try await client.banGroupUsers(session: session, groupId: groupId, ids: userIds)
Client
1
2
3
4
5
6
7
const groupId = '<group id>';
final userIds = ['<user id>'];
await client.banGroupUsers(
  session: session,
  groupId: groupId,
  userIds: userIds,
);
Client
1
2
3
String groupid = "<group id>";
String[] userIds = new String[] {"<user id>"};
client.banGroupUsers(session, groupid, userIds).get();
Client
1
2
3
4
5
POST /v2/group/<group id>/ban?user_ids=<user id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
local group_id = "<group id>"
local user_ids = { "<user id>" }
local result = client.ban_group_users(group_id, user_ids)

if result.error then
  print(result.message)
end

Remove a group #

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.

Client
1
2
curl -X DELETE "http://127.0.0.1:7350/v2/group/<group id>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
const group_id = "<group id>";
await client.deleteGroup(session, group_id);
Client
1
2
const string groupId = "<group id>";
await client.DeleteGroupAsync(session, groupId);
Client
1
2
let groupId = "<group id>"
try await client.deleteGroup(session: session, groupId: groupId)
Client
1
2
3
4
5
const groupId = '<group id>';
await client.deleteGroup(
  session: session,
  groupId: groupId,
);
Client
1
2
3
4
5
6
7
auto successCallback = []()
{
    std::cout << "group deleted" << std::endl;
};

string group_id = "<group id>";
client->deleteGroup(session, group_id, successCallback);
Client
1
2
String groupid = "<group id>";
client.deleteGroup(session, groupid).get();
Client
1
2
3
4
5
6
7
8
var group_id = "<group id>"
var remove : NakamaAsyncResult = yield(client.delete_group_async(session, group_id), "completed")

if remove.is_exception():
    print("An error occurred: %s" % remove)
    return

print("Group removed")
Client
1
2
3
4
5
6
7
8
var group_id = "<group id>"
var remove : NakamaAsyncResult = await client.delete_group_async(session, group_id)

if remove.is_exception():
    print("An error occurred: %s" % remove)
    return

print("Group removed")
Client
1
2
3
4
5
DELETE /v2/group/<group id>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
local group_id = "<group id>"
local result = client.delete_group(group_id)

if result.error then
  print(result.message)
end

Related Pages