Skip to content

Commit c7c2da2

Browse files
authored
Add User Permissions code examples + Add related missing properties + Add unit tests (#154)
* Add option to read & write app scoped permissions (grants) * fix naming * Add code examples related to user permissions + Change AddMembersAsync to return UpdateChannel + Add Members to UpdateChannelResponse + Add Grants to ChannelConfigBase + add ChannelRole to ChannelMember + add unit tests guaranteeing that the code examples work as stated * fix auto review errors * Allow settings grants explicitly to NULL. This is a special case where API will reset grants to default settings * Change test to create temporary channel types only (mac count is restricted by the API)
1 parent b097d75 commit c7c2da2

14 files changed

+466
-25
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using StreamChat.Clients;
2+
using StreamChat.Models;
3+
4+
namespace DocsExamples;
5+
6+
/// <summary>
7+
/// Code examples for <see href="https://getstream.io/chat/docs/python/user_permissions/"/>
8+
/// </summary>
9+
internal class UserPermissions
10+
{
11+
private readonly IUserClient _userClient;
12+
private readonly IChannelClient _channelClient;
13+
private readonly IPermissionClient _permissionClient;
14+
private readonly IChannelTypeClient _channelTypeClient;
15+
private readonly IAppClient _appClient;
16+
17+
public UserPermissions()
18+
{
19+
var factory = new StreamClientFactory("{{ api_key }}", "{{ api_secret }}");
20+
_userClient = factory.GetUserClient();
21+
_channelClient = factory.GetChannelClient();
22+
_permissionClient = factory.GetPermissionClient();
23+
_channelTypeClient = factory.GetChannelTypeClient();
24+
_appClient = factory.GetAppClient();
25+
}
26+
27+
internal async Task ChangeUserRole()
28+
{
29+
var upsertResponse = await _userClient.UpdatePartialAsync(new UserPartialRequest
30+
{
31+
Id = "user-id",
32+
Set = new Dictionary<string, object>
33+
{
34+
{ "role", "special_agent" }
35+
}
36+
});
37+
}
38+
39+
internal async Task VerifyChannelMemberRoleAssigned()
40+
{
41+
var addMembersResponse
42+
= await _channelClient.AddMembersAsync("channel-type", "channel-id", new[] { "user-id" });
43+
Console.WriteLine(addMembersResponse.Members[0].ChannelRole); // channel role is equal to "channel_member"
44+
}
45+
46+
internal async Task AssignRoles()
47+
{
48+
// User must be a member of the channel before you can assign channel role
49+
var resp = await _channelClient.AssignRolesAsync("channel-type", "channel-id", new AssignRoleRequest
50+
{
51+
AssignRoles = new List<RoleAssignment>
52+
{
53+
new RoleAssignment { UserId = "user-id", ChannelRole = Role.ChannelModerator }
54+
}
55+
});
56+
}
57+
58+
internal async Task CreateRole()
59+
{
60+
await _permissionClient.CreateRoleAsync("special_agent");
61+
}
62+
63+
internal async Task DeleteRole()
64+
{
65+
await _permissionClient.DeleteRoleAsync("special_agent");
66+
}
67+
68+
internal async Task ListPermissions()
69+
{
70+
var response = await _permissionClient.ListPermissionsAsync();
71+
}
72+
73+
internal async Task UpdateGrantedPermissions()
74+
{
75+
// observe current grants of the channel type
76+
var channelType = await _channelTypeClient.GetChannelTypeAsync("messaging");
77+
Console.WriteLine(channelType.Grants);
78+
79+
// update "channel_member" role grants in "messaging" scope
80+
var update = new ChannelTypeWithStringCommandsRequest
81+
{
82+
Grants = new Dictionary<string, List<string>>
83+
{
84+
{
85+
// This will replace all existing grants of "channel_member" role
86+
"channel_member", new List<string>
87+
{
88+
"read-channel", // allow access to the channel
89+
"create-message", // create messages in the channel
90+
"update-message-owner", // update own user messages
91+
"delete-message-owner", // delete own user messages
92+
}
93+
},
94+
}
95+
};
96+
await _channelTypeClient.UpdateChannelTypeAsync("messaging", update);
97+
}
98+
99+
internal async Task RemoveGrantedPermissionsByCategory()
100+
{
101+
var update = new ChannelTypeWithStringCommandsRequest
102+
{
103+
Grants = new Dictionary<string, List<string>>
104+
{
105+
{ "guest", new List<string>() }, // removes all grants of "guest" role
106+
{ "anonymous", new List<string>() }, // removes all grants of "anonymous" role
107+
}
108+
};
109+
await _channelTypeClient.UpdateChannelTypeAsync("messaging", update);
110+
}
111+
112+
internal async Task ResetGrantsToDefaultSettings()
113+
{
114+
var update = new ChannelTypeWithStringCommandsRequest
115+
{
116+
Grants = new Dictionary<string, List<string>>()
117+
};
118+
await _channelTypeClient.UpdateChannelTypeAsync("messaging", update);
119+
}
120+
121+
internal async Task UpdateAppScopedGrants()
122+
{
123+
var settings = new AppSettingsRequest
124+
{
125+
Grants = new Dictionary<string, List<string>>
126+
{
127+
{ "anonymous", new List<string>() },
128+
{ "guest", new List<string>() },
129+
{ "user", new List<string> { "search-user", "mute-user" } },
130+
{ "admin", new List<string> { "search-user", "mute-user", "ban-user" } },
131+
}
132+
};
133+
await _appClient.UpdateAppSettingsAsync(settings);
134+
}
135+
136+
internal async Task UpdateChannelLevelPermissions()
137+
{
138+
var grants = new Dictionary<string, object> { { "user", new List<string> { "!add-links", "create-reaction" } } };
139+
var overrides = new Dictionary<string, object> { { "grants", grants } };
140+
var request = new PartialUpdateChannelRequest
141+
{
142+
Set = new Dictionary<string, object>
143+
{
144+
{ "config_overrides", overrides }
145+
}
146+
};
147+
var resp = await _channelClient.PartialUpdateAsync("channel-type", "channel-id", request);
148+
}
149+
}

src/Clients/ChannelClient.Members.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ namespace StreamChat.Clients
88
{
99
public partial class ChannelClient
1010
{
11-
public async Task<ApiResponse> AddMembersAsync(string channelType, string channelId, params string[] userIds)
11+
public async Task<UpdateChannelResponse> AddMembersAsync(string channelType, string channelId, params string[] userIds)
1212
=> await AddMembersAsync(channelType, channelId, userIds, null, null);
1313

14-
public async Task<ApiResponse> AddMembersAsync(string channelType, string channelId, IEnumerable<string> userIds, MessageRequest msg, AddMemberOptions options)
15-
=> await ExecuteRequestAsync<ApiResponse>($"channels/{channelType}/{channelId}",
14+
public async Task<UpdateChannelResponse> AddMembersAsync(string channelType, string channelId, IEnumerable<string> userIds, MessageRequest msg, AddMemberOptions options)
15+
=> await ExecuteRequestAsync<UpdateChannelResponse>($"channels/{channelType}/{channelId}",
1616
HttpMethod.POST,
1717
HttpStatusCode.Created,
1818
new ChannelUpdateRequest

src/Clients/IChannelClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ public interface IChannelClient
1414
/// Adds members to a channel.
1515
/// </summary>
1616
/// <remarks>https://getstream.io/chat/docs/dotnet-csharp/channel_members/?language=csharp</remarks>
17-
Task<ApiResponse> AddMembersAsync(string channelType, string channelId, params string[] userIds);
17+
Task<UpdateChannelResponse> AddMembersAsync(string channelType, string channelId, params string[] userIds);
1818

1919
/// <summary>
2020
/// Adds members to a channel.
2121
/// </summary>
2222
/// <remarks>https://getstream.io/chat/docs/dotnet-csharp/channel_members/?language=csharp</remarks>
23-
Task<ApiResponse> AddMembersAsync(string channelType, string channelId, IEnumerable<string> userIds,
23+
Task<UpdateChannelResponse> AddMembersAsync(string channelType, string channelId, IEnumerable<string> userIds,
2424
MessageRequest msg, AddMemberOptions options);
2525

2626
/// <summary>

src/Models/Channel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class UpdateChannelResponse : ApiResponse
5151
{
5252
public ChannelWithConfig Channel { get; set; }
5353
public Message Message { get; set; }
54+
public List<ChannelMember> Members { get; set; }
5455
}
5556

5657
public class PartialUpdateChannelRequest

src/Models/ChannelConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public abstract class ChannelConfigBase
1818
public string MessageRetention { get; set; }
1919
public int MaxMessageLength { get; set; }
2020
public string Automod { get; set; }
21+
public Dictionary<string, List<string>> Grants { get; set; }
2122
}
2223

2324
public class ChannelConfig : ChannelConfigBase

src/Models/ChannelMember.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ChannelMember : CustomDataBase
1919
public DateTimeOffset? InviteAcceptedAt { get; set; }
2020
public DateTimeOffset? InviteRejectedAt { get; set; }
2121
public string Role { get; set; }
22+
public string ChannelRole { get; set; }
2223
public DateTimeOffset? CreatedAt { get; set; }
2324
public DateTimeOffset? UpdatedAt { get; set; }
2425
public bool? Banned { get; set; }

src/Models/ChannelType.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ public abstract class ChannelTypeRequestBase
6262
[Obsolete("Use V2 Permissions APIs instead. " +
6363
"See https://getstream.io/chat/docs/dotnet-csharp/migrating_from_legacy/?language=csharp")]
6464
public List<ChannelTypePermission> Permissions { get; set; }
65-
public Dictionary<string, List<string>> Grants { get; set; }
65+
66+
// JsonProperty is needed because passing NULL is a special case where API resets the grants to the default settings.
67+
// Empty Dictionary as a default value is needed in order for the default object to not reset the grants
68+
[JsonProperty(NullValueHandling = NullValueHandling.Include,
69+
DefaultValueHandling = DefaultValueHandling.Include)]
70+
public Dictionary<string, List<string>> Grants { get; set; } = new Dictionary<string, List<string>>();
6671
}
6772

6873
public class ChannelTypeWithCommandsRequest : ChannelTypeRequestBase

tests/BlocklistClientTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public async Task TearDownAsync()
3636
}
3737

3838
[Test]
39-
public Task TestGetAsync() => TryMultiple(async () =>
39+
public Task TestGetAsync() => TryMultipleAsync(async () =>
4040
{
4141
var resp = await _blocklistClient.GetAsync(_blocklistName);
4242

@@ -47,7 +47,7 @@ public Task TestGetAsync() => TryMultiple(async () =>
4747
});
4848

4949
[Test]
50-
public Task TestListAsync() => TryMultiple(async () =>
50+
public Task TestListAsync() => TryMultipleAsync(async () =>
5151
{
5252
var resp = await _blocklistClient.ListAsync();
5353

@@ -59,12 +59,12 @@ public async Task TestUpdateAsync()
5959
{
6060
var expectedWords = new[] { "test", "test2" };
6161

62-
await TryMultiple(async () =>
62+
await TryMultipleAsync(async () =>
6363
{
6464
await _blocklistClient.UpdateAsync(_blocklistName, expectedWords);
6565
});
6666

67-
await TryMultiple(async () =>
67+
await TryMultipleAsync(async () =>
6868
{
6969
var updated = await _blocklistClient.GetAsync(_blocklistName);
7070
updated.Blocklist.Words.Should().BeEquivalentTo(expectedWords);

tests/ChannelTypeClientTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ await WaitForAsync(async () =>
5252

5353
[Test]
5454
public Task TestGetChannelTypeAsync()
55-
=> TryMultiple(testBody: async () =>
55+
=> TryMultipleAsync(testBody: async () =>
5656
{
5757
var actualChannelType = await _channelTypeClient.GetChannelTypeAsync(_channelType.Name);
5858
actualChannelType.Name.Should().BeEquivalentTo(_channelType.Name);

tests/CommandClientTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public async Task TeardownAsync()
3838

3939
[Test]
4040
public Task TestGetCommandAsync()
41-
=> TryMultiple(async () =>
41+
=> TryMultipleAsync(async () =>
4242
{
4343
var command = await _commandClient.GetAsync(_command.Name);
4444

@@ -47,7 +47,7 @@ public Task TestGetCommandAsync()
4747

4848
[Test]
4949
public Task TestListCommandsAsync()
50-
=> TryMultiple(async () =>
50+
=> TryMultipleAsync(async () =>
5151
{
5252
var resp = await _commandClient.ListAsync();
5353

0 commit comments

Comments
 (0)