diff --git a/src/ChatClient.cs b/src/ChatClient.cs index b476322..0edf7c7 100644 --- a/src/ChatClient.cs +++ b/src/ChatClient.cs @@ -543,6 +543,24 @@ public async Task> GetManyMessagesAsync( return result; } + // Returns pinned messages for the channel + public async Task> GetPinnedMessagesAsync(string type, string id, object request = null, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["type"] = type, + ["id"] = id, + }; + var queryParams = ExtractQueryParams(request); + + var result = await _client.MakeRequestAsync( + "GET", + "/api/v2/chat/channels/{type}/{id}/pinned_messages", queryParams, null, pathParams, + cancellationToken); + return result; + } + // This Method creates a channel or returns an existing one with matching attributes // Sends events: @@ -1258,6 +1276,79 @@ public async Task> UnmuteChannelAsync(UnmuteChann return result; } + // Get all predefined filters with optional sorting by created_at, updated_at, name, or operation + public async Task> GetPredefinedFiltersAsync(object request = null, + CancellationToken cancellationToken = default) + { + var queryParams = ExtractQueryParams(request); + + var result = await _client.MakeRequestAsync( + "GET", + "/api/v2/chat/predefined_filters", queryParams, null, null, + cancellationToken); + return result; + } + + // Create a predefined filter that can be used in Query endpoints + public async Task> CreatePredefinedFilterAsync(CreatePredefinedFilterRequest request, + CancellationToken cancellationToken = default) + { + + var result = await _client.MakeRequestAsync( + "POST", + "/api/v2/chat/predefined_filters", null, request, null, + cancellationToken); + return result; + } + + // Delete a predefined filter by name + public async Task> DeletePredefinedFilterAsync(string name, object request = null, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["name"] = name, + }; + + var result = await _client.MakeRequestAsync( + "DELETE", + "/api/v2/chat/predefined_filters/{name}", null, null, pathParams, + cancellationToken); + return result; + } + + // Get a predefined filter by name + public async Task> GetPredefinedFilterAsync(string name, object request = null, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["name"] = name, + }; + + var result = await _client.MakeRequestAsync( + "GET", + "/api/v2/chat/predefined_filters/{name}", null, null, pathParams, + cancellationToken); + return result; + } + + // Update a predefined filter by name + public async Task> UpdatePredefinedFilterAsync(string name, UpdatePredefinedFilterRequest request, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["name"] = name, + }; + + var result = await _client.MakeRequestAsync( + "PUT", + "/api/v2/chat/predefined_filters/{name}", null, request, pathParams, + cancellationToken); + return result; + } + // Find and filter channel scoped or global user bans public async Task> QueryBannedUsersAsync(object request = null, CancellationToken cancellationToken = default) @@ -1517,6 +1608,10 @@ public async Task> QuerySegmentTarge // - Use 'start_date'/'end_date' parameters (YYYY-MM-DD format) for daily breakdown // - If neither provided, defaults to current month (monthly mode) + // **Team Filter:** + // - Use 'team' to return a single team's stats (empty string selects users not assigned to any team) + // - Mutually exclusive with the 'next' pagination cursor + // This endpoint is server-side only. public async Task> QueryTeamUsageStatsAsync(QueryTeamUsageStatsRequest request, CancellationToken cancellationToken = default) diff --git a/src/CommonClient.cs b/src/CommonClient.cs index 2e6b191..f8379d2 100644 --- a/src/CommonClient.cs +++ b/src/CommonClient.cs @@ -506,6 +506,32 @@ public async Task> ListPermissionsAsync( return result; } + public async Task> CreatePermissionAsync(CreatePermissionRequest request, + CancellationToken cancellationToken = default) + { + + var result = await MakeRequestAsync( + "POST", + "/api/v2/permissions", null, request, null, + cancellationToken); + + return result; + } + public async Task> DeletePermissionAsync(string id, object request = null, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["id"] = id, + }; + + var result = await MakeRequestAsync( + "DELETE", + "/api/v2/permissions/{id}", null, null, pathParams, + cancellationToken); + + return result; + } public async Task> GetPermissionAsync(string id, object request = null, CancellationToken cancellationToken = default) { @@ -521,6 +547,21 @@ public async Task> GetPermissionAsyn return result; } + public async Task> UpdatePermissionAsync(string id, PermissionRequest request, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["id"] = id, + }; + + var result = await MakeRequestAsync( + "PUT", + "/api/v2/permissions/{id}", null, request, pathParams, + cancellationToken); + + return result; + } public async Task> CreatePollAsync(CreatePollRequest request, CancellationToken cancellationToken = default) { diff --git a/src/Feed.cs b/src/Feed.cs index 8b2bd20..733d08e 100644 --- a/src/Feed.cs +++ b/src/Feed.cs @@ -73,6 +73,15 @@ public async Task> ChangeFeedVisibi request, cancellationToken); } + // Returns the number of activities in a feed, the total number of comments on those activities (including nested replies), and the sum of both. The comment total is cached for a few seconds on large feeds. + public async Task> GetFeedCountsAsync( + object request = null, + CancellationToken cancellationToken = default) + { + return await _client.GetFeedCountsAsync(_feedGroup, _feedId, + request, cancellationToken); + } + // Add, remove, or set members for a feed public async Task> UpdateFeedMembersAsync( UpdateFeedMembersRequest request, diff --git a/src/FeedsV3Client.cs b/src/FeedsV3Client.cs index 79984a8..7de9406 100644 --- a/src/FeedsV3Client.cs +++ b/src/FeedsV3Client.cs @@ -894,6 +894,22 @@ public async Task> ChangeFeedVisibi return result; } + public async Task> GetFeedCountsAsync(string feedGroupID, string feedID, object request = null, + CancellationToken cancellationToken = default) + { + var pathParams = new Dictionary + { + ["feed_group_id"] = feedGroupID, + ["feed_id"] = feedID, + }; + + var result = await _client.MakeRequestAsync( + "GET", + "/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}/counts", null, null, pathParams, + cancellationToken); + + return result; + } public async Task> UpdateFeedMembersAsync(string feedGroupID, string feedID, UpdateFeedMembersRequest request, CancellationToken cancellationToken = default) { diff --git a/src/models.cs b/src/models.cs index 6622ced..a3e6a1b 100644 --- a/src/models.cs +++ b/src/models.cs @@ -674,6 +674,16 @@ public class ActivityProcessorConfig /// [JsonPropertyName("type")] public string Type { get; set; } + /// + /// Minimum number of characters the activity text must have before this processor runs. 0 (the default) disables the check. Only applies to text_interest_tags. + /// + [JsonPropertyName("min_text_length")] + public int? MinTextLength { get; set; } + /// + /// Minimum number of words the activity text must have before this processor runs. 0 (the default) disables the check. Only applies to text_interest_tags. Words are whitespace-separated, so scripts written without word spacing (Chinese, Japanese, Thai) always count as 1 word regardless of length — use min_text_length for those. + /// + [JsonPropertyName("min_word_count")] + public int? MinWordCount { get; set; } } public class ActivityReactionAddedEvent @@ -3591,7 +3601,7 @@ public class BulkActionAppealsRequest [JsonPropertyName("reject_appeal")] public RejectAppealRequestPayload? RejectAppeal { get; set; } /// - /// Configuration for restore action + /// Configuration for restore action. State-aware: reverses whichever of a delete, a block, or a shadow block currently applies to the content (including both a delete and a block/shadow block at once). /// [JsonPropertyName("restore")] public RestoreActionRequestPayload? Restore { get; set; } @@ -3601,7 +3611,7 @@ public class BulkActionAppealsRequest [JsonPropertyName("unban")] public UnbanActionRequestPayload? Unban { get; set; } /// - /// Configuration for unblock action + /// Deprecated: use restore instead — it now also reverses a block or shadow block. Configuration for unblock action. /// [JsonPropertyName("unblock")] public UnblockActionRequestPayload? Unblock { get; set; } @@ -5877,6 +5887,8 @@ public class ChannelConfig public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -6031,6 +6043,8 @@ public class ChannelConfigWithInfo public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -7036,6 +7050,8 @@ public class ChannelTypeConfig public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -7899,6 +7915,8 @@ public class Classification public double? Confidence { get; set; } [JsonPropertyName("severity")] public string? Severity { get; set; } + [JsonPropertyName("matched_contributors")] + public List MatchedContributors { get; set; } [JsonPropertyName("subclassifications")] public List Subclassifications { get; set; } } @@ -9117,6 +9135,8 @@ public class CreateChannelTypeResponse public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -9574,6 +9594,45 @@ public class CreateMembershipLevelResponse public MembershipLevelResponse MembershipLevel { get; set; } } + public class CreatePermissionRequest + { + /// + /// Action name this permission is for (e.g. SendMessage) + /// + [JsonPropertyName("action")] + public string Action { get; set; } + /// + /// Unique permission ID + /// + [JsonPropertyName("id")] + public string ID { get; set; } + /// + /// Name of the permission + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// MongoDB style condition which decides whether or not the permission is granted + /// + [JsonPropertyName("condition")] + public object Condition { get; set; } + /// + /// Description of the permission + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + /// + /// Whether this permission applies to resource owner or not + /// + [JsonPropertyName("owner")] + public bool? Owner { get; set; } + /// + /// Whether this permission applies to teammates (multi-tenancy mode only) + /// + [JsonPropertyName("same_team")] + public bool? SameTeam { get; set; } + } + public class CreatePolicyTestSetRequest { /// @@ -9680,6 +9739,43 @@ public class CreatePollRequest public UserRequest? User { get; set; } } + public class CreatePredefinedFilterRequest + { + /// + /// The unique name of the predefined filter (alphanumeric, _, - only) + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// The operation this filter is for (e.g., QueryChannels) + /// + [JsonPropertyName("operation")] + public string Operation { get; set; } + /// + /// Filter to apply to the query + /// + [JsonPropertyName("filter")] + public object Filter { get; set; } + /// + /// The description of the predefined filter + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + [JsonPropertyName("sort")] + public List Sort { get; set; } + } + + public class CreatePredefinedFilterResponse + { + /// + /// Duration of the request in milliseconds + /// + [JsonPropertyName("duration")] + public string Duration { get; set; } + [JsonPropertyName("predefined_filter")] + public PredefinedFilterResponse? PredefinedFilter { get; set; } + } + public class CreateQueueRequest { [JsonPropertyName("name")] @@ -13937,6 +14033,8 @@ public class GetChannelTypeResponse public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -14177,6 +14275,27 @@ public class GetExternalStorageResponse public GetExternalStorageGCSResponse? Gcs { get; set; } } + public class GetFeedCountsResponse + { + /// + /// Number of activities in the feed + /// + [JsonPropertyName("activity_count")] + public int ActivityCount { get; set; } + /// + /// Total number of comments on those activities, including nested replies + /// + [JsonPropertyName("comment_count")] + public int CommentCount { get; set; } + [JsonPropertyName("duration")] + public string Duration { get; set; } + /// + /// Sum of activity_count and comment_count + /// + [JsonPropertyName("total_count")] + public int TotalCount { get; set; } + } + public class GetFeedGroupResponse { [JsonPropertyName("duration")] @@ -14685,6 +14804,31 @@ public class GetOrCreateUnfollowResponse public FollowResponse? Follow { get; set; } } + public class GetPinnedMessagesResponse + { + /// + /// Duration of the request in milliseconds + /// + [JsonPropertyName("duration")] + public string Duration { get; set; } + /// + /// Messages + /// + [JsonPropertyName("messages")] + public List Messages { get; set; } + } + + public class GetPredefinedFilterResponse + { + /// + /// Duration of the request in milliseconds + /// + [JsonPropertyName("duration")] + public string Duration { get; set; } + [JsonPropertyName("predefined_filter")] + public PredefinedFilterResponse? PredefinedFilter { get; set; } + } + public class GetPushTemplatesResponse { /// @@ -19802,6 +19946,28 @@ public class Percentiles public double? P95 { get; set; } } + public class PerformanceAnalysisResponse + { + [JsonPropertyName("analysis_type")] + public string AnalysisType { get; set; } + [JsonPropertyName("score")] + public string Score { get; set; } + [JsonPropertyName("indexed_fields")] + public List IndexedFields { get; set; } + [JsonPropertyName("recommendations")] + public List Recommendations { get; set; } + [JsonPropertyName("unindexed_fields")] + public List UnindexedFields { get; set; } + [JsonPropertyName("unindexed_sort_fields")] + public List UnindexedSortFields { get; set; } + [JsonPropertyName("warnings")] + public List Warnings { get; set; } + [JsonPropertyName("last_analyzed")] + public DateTime? LastAnalyzed { get; set; } + [JsonPropertyName("scan_type")] + public string? ScanType { get; set; } + } + public class Permission { /// @@ -19861,6 +20027,40 @@ public class Permission public object Condition { get; set; } } + public class PermissionRequest + { + /// + /// Action name this permission is for (e.g. SendMessage) + /// + [JsonPropertyName("action")] + public string Action { get; set; } + /// + /// Name of the permission + /// + [JsonPropertyName("name")] + public string Name { get; set; } + /// + /// MongoDB style condition which decides whether or not the permission is granted + /// + [JsonPropertyName("condition")] + public object Condition { get; set; } + /// + /// Description of the permission + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + /// + /// Whether this permission applies to resource owner or not + /// + [JsonPropertyName("owner")] + public bool? Owner { get; set; } + /// + /// Whether this permission applies to teammates (multi-tenancy mode only) + /// + [JsonPropertyName("same_team")] + public bool? SameTeam { get; set; } + } + public class PermissionRequestEvent { [JsonPropertyName("call_cid")] @@ -20390,6 +20590,40 @@ public class PoorTail public double? HealthyPct { get; set; } } + public class PredefinedFilterResponse + { + [JsonPropertyName("created_at")] + public DateTime CreatedAt { get; set; } + [JsonPropertyName("name")] + public string Name { get; set; } + [JsonPropertyName("operation")] + public string Operation { get; set; } + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } + [JsonPropertyName("filter")] + public object Filter { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } + [JsonPropertyName("query_id")] + public int? QueryID { get; set; } + [JsonPropertyName("sort")] + public List Sort { get; set; } + [JsonPropertyName("performance")] + public PerformanceAnalysisResponse? Performance { get; set; } + [JsonPropertyName("stats")] + public PredefinedFilterStatsResponse? Stats { get; set; } + } + + public class PredefinedFilterStatsResponse + { + [JsonPropertyName("calls")] + public int Calls { get; set; } + [JsonPropertyName("max_latency_ms")] + public int MaxLatencyMs { get; set; } + [JsonPropertyName("last_seen")] + public DateTime? LastSeen { get; set; } + } + public class PrivacySettingsResponse { [JsonPropertyName("delivery_receipts")] @@ -20520,7 +20754,7 @@ public class PushNotificationConfig [JsonPropertyName("enable_push")] public bool? EnablePush { get; set; } /// - /// List of notification types that should trigger push notifications (e.g., follow, comment, reaction, comment_reaction, mention) + /// Allowlist of notification types that may trigger push (e.g. follow, comment, reaction, comment_reaction, mention, or any custom activity.type). Empty or omitted means no types. Built-in notifications match notification_context.trigger.type; manually added notification activities match activity.type. /// [JsonPropertyName("push_types")] public List PushTypes { get; set; } @@ -22515,6 +22749,24 @@ public class QueryPollsResponse public string? Prev { get; set; } } + public class QueryPredefinedFiltersResponse + { + /// + /// Duration of the request in milliseconds + /// + [JsonPropertyName("duration")] + public string Duration { get; set; } + /// + /// Predefined filters + /// + [JsonPropertyName("predefined_filters")] + public List PredefinedFilters { get; set; } + [JsonPropertyName("next")] + public string? Next { get; set; } + [JsonPropertyName("prev")] + public string? Prev { get; set; } + } + public class QueryReactionsRequest { [JsonPropertyName("limit")] @@ -22824,6 +23076,11 @@ public class QueryTeamUsageStatsRequest /// [JsonPropertyName("start_date")] public string? StartDate { get; set; } + /// + /// Filter results to a single team ID. Empty string selects users not assigned to any team. Mutually exclusive with 'next'. + /// + [JsonPropertyName("team")] + public string? Team { get; set; } } public class QueryTeamUsageStatsResponse @@ -25911,6 +26168,16 @@ public class SipInboundCredentials public object UserCustomData { get; set; } } + public class SortParam + { + [JsonPropertyName("direction")] + public int Direction { get; set; } + [JsonPropertyName("field")] + public string Field { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + } + public class SortParamRequest { /// @@ -26370,7 +26637,7 @@ public class SubmitActionRequest [JsonPropertyName("reject_appeal")] public RejectAppealRequestPayload? RejectAppeal { get; set; } /// - /// Configuration for restore action + /// Configuration for restore action. State-aware: reverses whichever of a delete, a block, or a shadow block currently applies to the content (including both a delete and a block/shadow block at once). /// [JsonPropertyName("restore")] public RestoreActionRequestPayload? Restore { get; set; } @@ -26385,7 +26652,7 @@ public class SubmitActionRequest [JsonPropertyName("unban")] public UnbanActionRequestPayload? Unban { get; set; } /// - /// Configuration for unblock action + /// Deprecated: use restore instead — it now also reverses a block or shadow block. Configuration for unblock action. /// [JsonPropertyName("unblock")] public UnblockActionRequestPayload? Unblock { get; set; } @@ -28409,6 +28676,8 @@ public class UpdateChannelTypeRequest public bool? DeliveryEvents { get; set; } [JsonPropertyName("mark_messages_pending")] public bool? MarkMessagesPending { get; set; } + [JsonPropertyName("message_retention")] + public string? MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool? Mutes { get; set; } [JsonPropertyName("partition_size")] @@ -28489,6 +28758,8 @@ public class UpdateChannelTypeResponse public bool MarkMessagesPending { get; set; } [JsonPropertyName("max_message_length")] public int MaxMessageLength { get; set; } + [JsonPropertyName("message_retention")] + public string MessageRetention { get; set; } [JsonPropertyName("mutes")] public bool Mutes { get; set; } [JsonPropertyName("name")] @@ -29337,6 +29608,38 @@ public class UpdatePollRequest public UserRequest? User { get; set; } } + public class UpdatePredefinedFilterRequest + { + /// + /// The operation this filter is for (e.g., QueryChannels) + /// + [JsonPropertyName("operation")] + public string Operation { get; set; } + /// + /// Filter to apply to the query + /// + [JsonPropertyName("filter")] + public object Filter { get; set; } + /// + /// The description of the predefined filter + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + [JsonPropertyName("sort")] + public List Sort { get; set; } + } + + public class UpdatePredefinedFilterResponse + { + /// + /// Duration of the request in milliseconds + /// + [JsonPropertyName("duration")] + public string Duration { get; set; } + [JsonPropertyName("predefined_filter")] + public PredefinedFilterResponse? PredefinedFilter { get; set; } + } + public class UpdateQueueRequest { [JsonPropertyName("description")] diff --git a/tests/FeedTests.cs b/tests/FeedTests.cs index 596b664..1a30547 100644 --- a/tests/FeedTests.cs +++ b/tests/FeedTests.cs @@ -2173,6 +2173,43 @@ public async Task ChangeFeedVisibilityAsync_ShouldCallCorrectEndpoint() It.IsAny()), Times.Once); } [Test] + public async Task GetFeedCountsAsync_ShouldCallCorrectEndpoint() + { + // Arrange + object request = null!; + var feedGroupID = "test-feedGroupID"; + var feedID = "test-feedID"; + + var expectedResponse = new StreamResponse + { + Data = new GetFeedCountsResponse() + }; + + _mockClient.Setup(x => x.MakeRequestAsync( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _client.GetFeedCountsAsync(feedGroupID, feedID, null!); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(expectedResponse)); + + _mockClient.Verify(x => x.MakeRequestAsync( + "GET", + "/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}/counts", + It.IsAny>(), + It.IsAny(), + It.IsAny>(), + It.IsAny()), Times.Once); + } + [Test] public async Task UpdateFeedMembersAsync_ShouldCallCorrectEndpoint() { // Arrange