diff --git a/lib/getstream_ruby/generated/common_client.rb b/lib/getstream_ruby/generated/common_client.rb index 475ab20..07ee74b 100644 --- a/lib/getstream_ruby/generated/common_client.rb +++ b/lib/getstream_ruby/generated/common_client.rb @@ -1027,6 +1027,175 @@ def upload_image(image_upload_request) ) end + # Lists user groups with cursor-based pagination + # + # @param limit [Integer] + # @param id_gt [String] + # @param created_at_gt [String] + # @param team_id [String] + # @return [Models::ListUserGroupsResponse] + def list_user_groups(limit = nil, id_gt = nil, created_at_gt = nil, team_id = nil) + path = '/api/v2/usergroups' + # Build query parameters + query_params = {} + query_params['limit'] = limit unless limit.nil? + query_params['id_gt'] = id_gt unless id_gt.nil? + query_params['created_at_gt'] = created_at_gt unless created_at_gt.nil? + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Creates a new user group, optionally with initial members + # + # @param create_user_group_request [CreateUserGroupRequest] + # @return [Models::CreateUserGroupResponse] + def create_user_group(create_user_group_request) + path = '/api/v2/usergroups' + # Build request body + body = create_user_group_request + + # Make the API request + @client.make_request( + :post, + path, + body: body + ) + end + + # Searches user groups by name prefix for autocomplete + # + # @param query [String] + # @param limit [Integer] + # @param name_gt [String] + # @param id_gt [String] + # @param team_id [String] + # @return [Models::SearchUserGroupsResponse] + def search_user_groups(query, limit = nil, name_gt = nil, id_gt = nil, team_id = nil) + path = '/api/v2/usergroups/search' + # Build query parameters + query_params = {} + query_params['query'] = query unless query.nil? + query_params['limit'] = limit unless limit.nil? + query_params['name_gt'] = name_gt unless name_gt.nil? + query_params['id_gt'] = id_gt unless id_gt.nil? + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Deletes a user group and all its members + # + # @param _id [String] + # @param team_id [String] + # @return [Models::Response] + def delete_user_group(_id, team_id = nil) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build query parameters + query_params = {} + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :delete, + path, + query_params: query_params + ) + end + + # Gets a user group by ID, including its members + # + # @param _id [String] + # @param team_id [String] + # @return [Models::GetUserGroupResponse] + def get_user_group(_id, team_id = nil) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build query parameters + query_params = {} + query_params['team_id'] = team_id unless team_id.nil? + + # Make the API request + @client.make_request( + :get, + path, + query_params: query_params + ) + end + + # Updates a user group's name and/or description. team_id is immutable. + # + # @param _id [String] + # @param update_user_group_request [UpdateUserGroupRequest] + # @return [Models::UpdateUserGroupResponse] + def update_user_group(_id, update_user_group_request) + path = '/api/v2/usergroups/{id}' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build request body + body = update_user_group_request + + # Make the API request + @client.make_request( + :put, + path, + body: body + ) + end + + # Removes members from a user group. Users already not in the group are silently ignored. + # + # @param _id [String] + # @param remove_user_group_members_request [RemoveUserGroupMembersRequest] + # @return [Models::RemoveUserGroupMembersResponse] + def remove_user_group_members(_id, remove_user_group_members_request) + path = '/api/v2/usergroups/{id}/members' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build request body + body = remove_user_group_members_request + + # Make the API request + @client.make_request( + :delete, + path, + body: body + ) + end + + # Adds members to a user group. All user IDs must exist. The operation is all-or-nothing. + # + # @param _id [String] + # @param add_user_group_members_request [AddUserGroupMembersRequest] + # @return [Models::AddUserGroupMembersResponse] + def add_user_group_members(_id, add_user_group_members_request) + path = '/api/v2/usergroups/{id}/members' + # Replace path parameters + path = path.gsub('{id}', _id.to_s) + # Build request body + body = add_user_group_members_request + + # Make the API request + @client.make_request( + :post, + path, + body: body + ) + end + # Find and filter users # # @param payload [QueryUsersPayload] diff --git a/lib/getstream_ruby/generated/feeds_client.rb b/lib/getstream_ruby/generated/feeds_client.rb index 42285b7..412bab3 100644 --- a/lib/getstream_ruby/generated/feeds_client.rb +++ b/lib/getstream_ruby/generated/feeds_client.rb @@ -481,15 +481,15 @@ def delete_collections(collection_refs) # Read collections with optional filtering by user ID and collection name. By default, users can only read their own collections. # - # @param collection_refs [Array] # @param user_id [String] + # @param collection_refs [Array] # @return [Models::ReadCollectionsResponse] - def read_collections(collection_refs, user_id = nil) + def read_collections(user_id = nil, collection_refs = nil) path = '/api/v2/feeds/collections' # Build query parameters query_params = {} - query_params['collection_refs'] = collection_refs unless collection_refs.nil? query_params['user_id'] = user_id unless user_id.nil? + query_params['collection_refs'] = collection_refs unless collection_refs.nil? # Make the API request @client.make_request( @@ -1098,6 +1098,22 @@ def get_follow_suggestions(feed_group_id, limit = nil, user_id = nil) ) end + # Restores a soft-deleted feed group by its ID. Only clears DeletedAt in the database; no other fields are updated. + # + # @param feed_group_id [String] + # @return [Models::RestoreFeedGroupResponse] + def restore_feed_group(feed_group_id) + path = '/api/v2/feeds/feed_groups/{feed_group_id}/restore' + # Replace path parameters + path = path.gsub('{feed_group_id}', feed_group_id.to_s) + + # Make the API request + @client.make_request( + :post, + path + ) + end + # Delete a feed group by its ID. Can perform a soft delete (default) or hard delete. # # @param _id [String] diff --git a/lib/getstream_ruby/generated/models/add_user_group_members_request.rb b/lib/getstream_ruby/generated/models/add_user_group_members_request.rb new file mode 100644 index 0000000..544fc5a --- /dev/null +++ b/lib/getstream_ruby/generated/models/add_user_group_members_request.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for adding members to a user group + class AddUserGroupMembersRequest < GetStream::BaseModel + + # Model attributes + # @!attribute member_ids + # @return [Array] List of user IDs to add as members + attr_accessor :member_ids + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @member_ids = attributes[:member_ids] || attributes['member_ids'] + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + member_ids: 'member_ids', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/add_user_group_members_response.rb b/lib/getstream_ruby/generated/models/add_user_group_members_response.rb new file mode 100644 index 0000000..31f4bec --- /dev/null +++ b/lib/getstream_ruby/generated/models/add_user_group_members_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for adding members to a user group + class AddUserGroupMembersResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/app_response_fields.rb b/lib/getstream_ruby/generated/models/app_response_fields.rb index e11186f..ae078a2 100644 --- a/lib/getstream_ruby/generated/models/app_response_fields.rb +++ b/lib/getstream_ruby/generated/models/app_response_fields.rb @@ -48,6 +48,9 @@ class AppResponseFields < GetStream::BaseModel # @!attribute max_aggregated_activities_length # @return [Integer] attr_accessor :max_aggregated_activities_length + # @!attribute moderation_audio_call_moderation_enabled + # @return [Boolean] + attr_accessor :moderation_audio_call_moderation_enabled # @!attribute moderation_enabled # @return [Boolean] attr_accessor :moderation_enabled @@ -57,6 +60,9 @@ class AppResponseFields < GetStream::BaseModel # @!attribute moderation_multitenant_blocklist_enabled # @return [Boolean] attr_accessor :moderation_multitenant_blocklist_enabled + # @!attribute moderation_video_call_moderation_enabled + # @return [Boolean] + attr_accessor :moderation_video_call_moderation_enabled # @!attribute moderation_webhook_url # @return [String] attr_accessor :moderation_webhook_url @@ -179,9 +185,11 @@ def initialize(attributes = {}) @id = attributes[:id] || attributes['id'] @image_moderation_enabled = attributes[:image_moderation_enabled] || attributes['image_moderation_enabled'] @max_aggregated_activities_length = attributes[:max_aggregated_activities_length] || attributes['max_aggregated_activities_length'] + @moderation_audio_call_moderation_enabled = attributes[:moderation_audio_call_moderation_enabled] || attributes['moderation_audio_call_moderation_enabled'] @moderation_enabled = attributes[:moderation_enabled] || attributes['moderation_enabled'] @moderation_llm_configurability_enabled = attributes[:moderation_llm_configurability_enabled] || attributes['moderation_llm_configurability_enabled'] @moderation_multitenant_blocklist_enabled = attributes[:moderation_multitenant_blocklist_enabled] || attributes['moderation_multitenant_blocklist_enabled'] + @moderation_video_call_moderation_enabled = attributes[:moderation_video_call_moderation_enabled] || attributes['moderation_video_call_moderation_enabled'] @moderation_webhook_url = attributes[:moderation_webhook_url] || attributes['moderation_webhook_url'] @multi_tenant_enabled = attributes[:multi_tenant_enabled] || attributes['multi_tenant_enabled'] @name = attributes[:name] || attributes['name'] @@ -235,9 +243,11 @@ def self.json_field_mappings id: 'id', image_moderation_enabled: 'image_moderation_enabled', max_aggregated_activities_length: 'max_aggregated_activities_length', + moderation_audio_call_moderation_enabled: 'moderation_audio_call_moderation_enabled', moderation_enabled: 'moderation_enabled', moderation_llm_configurability_enabled: 'moderation_llm_configurability_enabled', moderation_multitenant_blocklist_enabled: 'moderation_multitenant_blocklist_enabled', + moderation_video_call_moderation_enabled: 'moderation_video_call_moderation_enabled', moderation_webhook_url: 'moderation_webhook_url', multi_tenant_enabled: 'multi_tenant_enabled', name: 'name', diff --git a/lib/getstream_ruby/generated/models/async_export_error_event.rb b/lib/getstream_ruby/generated/models/async_export_error_event.rb index e297b0e..1e922a8 100644 --- a/lib/getstream_ruby/generated/models/async_export_error_event.rb +++ b/lib/getstream_ruby/generated/models/async_export_error_event.rb @@ -43,7 +43,7 @@ def initialize(attributes = {}) @started_at = attributes[:started_at] || attributes['started_at'] @task_id = attributes[:task_id] || attributes['task_id'] @custom = attributes[:custom] || attributes['custom'] - @type = attributes[:type] || attributes['type'] || "export.channels.error" + @type = attributes[:type] || attributes['type'] || "export.users.error" @received_at = attributes[:received_at] || attributes['received_at'] || nil end diff --git a/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb b/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb index 93720c9..b00fe08 100644 --- a/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb +++ b/lib/getstream_ruby/generated/models/aws_rekognition_rule.rb @@ -18,6 +18,9 @@ class AWSRekognitionRule < GetStream::BaseModel # @!attribute min_confidence # @return [Float] attr_accessor :min_confidence + # @!attribute subclassifications + # @return [Hash] + attr_accessor :subclassifications # Initialize with attributes def initialize(attributes = {}) @@ -25,6 +28,7 @@ def initialize(attributes = {}) @action = attributes[:action] || attributes['action'] @label = attributes[:label] || attributes['label'] @min_confidence = attributes[:min_confidence] || attributes['min_confidence'] + @subclassifications = attributes[:subclassifications] || attributes['subclassifications'] || nil end # Override field mappings for JSON serialization @@ -32,7 +36,8 @@ def self.json_field_mappings { action: 'action', label: 'label', - min_confidence: 'min_confidence' + min_confidence: 'min_confidence', + subclassifications: 'subclassifications' } end end diff --git a/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb b/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb index f3b0310..6a0c82c 100644 --- a/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb +++ b/lib/getstream_ruby/generated/models/call_stats_report_ready_event.rb @@ -21,6 +21,12 @@ class CallStatsReportReadyEvent < GetStream::BaseModel # @!attribute type # @return [String] The type of event, "call.report_ready" in this case attr_accessor :type + # @!attribute is_trimmed + # @return [Boolean] Whether participants_overview is truncated by the server-side limit + attr_accessor :is_trimmed + # @!attribute participants_overview + # @return [Array] Top participant sessions overview + attr_accessor :participants_overview # Initialize with attributes def initialize(attributes = {}) @@ -29,6 +35,8 @@ def initialize(attributes = {}) @created_at = attributes[:created_at] || attributes['created_at'] @session_id = attributes[:session_id] || attributes['session_id'] @type = attributes[:type] || attributes['type'] || "call.stats_report_ready" + @is_trimmed = attributes[:is_trimmed] || attributes['is_trimmed'] || nil + @participants_overview = attributes[:participants_overview] || attributes['participants_overview'] || nil end # Override field mappings for JSON serialization @@ -37,7 +45,9 @@ def self.json_field_mappings call_cid: 'call_cid', created_at: 'created_at', session_id: 'session_id', - type: 'type' + type: 'type', + is_trimmed: 'is_trimmed', + participants_overview: 'participants_overview' } end end diff --git a/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb b/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb new file mode 100644 index 0000000..78350b6 --- /dev/null +++ b/lib/getstream_ruby/generated/models/call_violation_count_parameters.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class CallViolationCountParameters < GetStream::BaseModel + + # Model attributes + # @!attribute threshold + # @return [Integer] + attr_accessor :threshold + # @!attribute time_window + # @return [String] + attr_accessor :time_window + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @threshold = attributes[:threshold] || attributes['threshold'] || nil + @time_window = attributes[:time_window] || attributes['time_window'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + threshold: 'threshold', + time_window: 'time_window' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/config_response.rb b/lib/getstream_ruby/generated/models/config_response.rb index f590f1a..c8c1668 100644 --- a/lib/getstream_ruby/generated/models/config_response.rb +++ b/lib/getstream_ruby/generated/models/config_response.rb @@ -30,6 +30,9 @@ class ConfigResponse < GetStream::BaseModel # @!attribute ai_image_config # @return [AIImageConfig] attr_accessor :ai_image_config + # @!attribute ai_image_subclassifications + # @return [Hash>] Available L2 subclassifications per L1 image moderation label, based on the active provider + attr_accessor :ai_image_subclassifications # @!attribute ai_text_config # @return [AITextConfig] attr_accessor :ai_text_config @@ -68,6 +71,7 @@ def initialize(attributes = {}) @updated_at = attributes[:updated_at] || attributes['updated_at'] @supported_video_call_harm_types = attributes[:supported_video_call_harm_types] || attributes['supported_video_call_harm_types'] @ai_image_config = attributes[:ai_image_config] || attributes['ai_image_config'] || nil + @ai_image_subclassifications = attributes[:ai_image_subclassifications] || attributes['ai_image_subclassifications'] || nil @ai_text_config = attributes[:ai_text_config] || attributes['ai_text_config'] || nil @ai_video_config = attributes[:ai_video_config] || attributes['ai_video_config'] || nil @automod_platform_circumvention_config = attributes[:automod_platform_circumvention_config] || attributes['automod_platform_circumvention_config'] || nil @@ -89,6 +93,7 @@ def self.json_field_mappings updated_at: 'updated_at', supported_video_call_harm_types: 'supported_video_call_harm_types', ai_image_config: 'ai_image_config', + ai_image_subclassifications: 'ai_image_subclassifications', ai_text_config: 'ai_text_config', ai_video_config: 'ai_video_config', automod_platform_circumvention_config: 'automod_platform_circumvention_config', diff --git a/lib/getstream_ruby/generated/models/create_import_request.rb b/lib/getstream_ruby/generated/models/create_import_request.rb index 5586f27..88849bb 100644 --- a/lib/getstream_ruby/generated/models/create_import_request.rb +++ b/lib/getstream_ruby/generated/models/create_import_request.rb @@ -15,19 +15,24 @@ class CreateImportRequest < GetStream::BaseModel # @!attribute path # @return [String] attr_accessor :path + # @!attribute merge_custom + # @return [Boolean] + attr_accessor :merge_custom # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] @path = attributes[:path] || attributes['path'] + @merge_custom = attributes[:merge_custom] || attributes['merge_custom'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { mode: 'mode', - path: 'path' + path: 'path', + merge_custom: 'merge_custom' } end end diff --git a/lib/getstream_ruby/generated/models/create_user_group_request.rb b/lib/getstream_ruby/generated/models/create_user_group_request.rb new file mode 100644 index 0000000..cd00dee --- /dev/null +++ b/lib/getstream_ruby/generated/models/create_user_group_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for creating a user group + class CreateUserGroupRequest < GetStream::BaseModel + + # Model attributes + # @!attribute name + # @return [String] The user friendly name of the user group + attr_accessor :name + # @!attribute description + # @return [String] An optional description for the group + attr_accessor :description + # @!attribute id + # @return [String] Optional user group ID. If not provided, a UUID v7 will be generated + attr_accessor :id + # @!attribute team_id + # @return [String] Optional team ID to scope the group to a team + attr_accessor :team_id + # @!attribute member_ids + # @return [Array] Optional initial list of user IDs to add as members + attr_accessor :member_ids + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @name = attributes[:name] || attributes['name'] + @description = attributes[:description] || attributes['description'] || nil + @id = attributes[:id] || attributes['id'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @member_ids = attributes[:member_ids] || attributes['member_ids'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + name: 'name', + description: 'description', + id: 'id', + team_id: 'team_id', + member_ids: 'member_ids' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/create_user_group_response.rb b/lib/getstream_ruby/generated/models/create_user_group_response.rb new file mode 100644 index 0000000..4b664dc --- /dev/null +++ b/lib/getstream_ruby/generated/models/create_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for creating a user group + class CreateUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/daily_value.rb b/lib/getstream_ruby/generated/models/daily_value.rb new file mode 100644 index 0000000..297fd19 --- /dev/null +++ b/lib/getstream_ruby/generated/models/daily_value.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Metric value for a specific date + class DailyValue < GetStream::BaseModel + + # Model attributes + # @!attribute date + # @return [String] Date in YYYY-MM-DD format + attr_accessor :date + # @!attribute value + # @return [Integer] Metric value for this date + attr_accessor :value + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @date = attributes[:date] || attributes['date'] + @value = attributes[:value] || attributes['value'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + date: 'date', + value: 'value' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb b/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb index 610fc58..35ad713 100644 --- a/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_activity_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteActivityRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the activity to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity (required for delete_activity to distinguish v2 vs v3) + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the activity attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteActivityRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb b/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb index 09b15e1..b9df98a 100644 --- a/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_comment_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteCommentRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the comment to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the comment attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteCommentRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_message_request_payload.rb b/lib/getstream_ruby/generated/models/delete_message_request_payload.rb index 14dbfc9..9a027d3 100644 --- a/lib/getstream_ruby/generated/models/delete_message_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_message_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteMessageRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the message to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the message attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteMessageRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb b/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb index afb3968..8ae00ee 100644 --- a/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_reaction_request_payload.rb @@ -9,6 +9,12 @@ module Models class DeleteReactionRequestPayload < GetStream::BaseModel # Model attributes + # @!attribute entity_id + # @return [String] ID of the reaction to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the reaction attr_accessor :hard_delete @@ -19,6 +25,8 @@ class DeleteReactionRequestPayload < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @reason = attributes[:reason] || attributes['reason'] || nil end @@ -26,6 +34,8 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', reason: 'reason' } diff --git a/lib/getstream_ruby/generated/models/delete_user_request_payload.rb b/lib/getstream_ruby/generated/models/delete_user_request_payload.rb index 1ba276b..e25dde7 100644 --- a/lib/getstream_ruby/generated/models/delete_user_request_payload.rb +++ b/lib/getstream_ruby/generated/models/delete_user_request_payload.rb @@ -15,6 +15,12 @@ class DeleteUserRequestPayload < GetStream::BaseModel # @!attribute delete_feeds_content # @return [Boolean] Delete flagged feeds content attr_accessor :delete_feeds_content + # @!attribute entity_id + # @return [String] ID of the user to delete (alternative to item_id) + attr_accessor :entity_id + # @!attribute entity_type + # @return [String] Type of the entity + attr_accessor :entity_type # @!attribute hard_delete # @return [Boolean] Whether to permanently delete the user attr_accessor :hard_delete @@ -30,6 +36,8 @@ def initialize(attributes = {}) super(attributes) @delete_conversation_channels = attributes[:delete_conversation_channels] || attributes['delete_conversation_channels'] || nil @delete_feeds_content = attributes[:delete_feeds_content] || attributes['delete_feeds_content'] || nil + @entity_id = attributes[:entity_id] || attributes['entity_id'] || nil + @entity_type = attributes[:entity_type] || attributes['entity_type'] || nil @hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil @mark_messages_deleted = attributes[:mark_messages_deleted] || attributes['mark_messages_deleted'] || nil @reason = attributes[:reason] || attributes['reason'] || nil @@ -40,6 +48,8 @@ def self.json_field_mappings { delete_conversation_channels: 'delete_conversation_channels', delete_feeds_content: 'delete_feeds_content', + entity_id: 'entity_id', + entity_type: 'entity_type', hard_delete: 'hard_delete', mark_messages_deleted: 'mark_messages_deleted', reason: 'reason' diff --git a/lib/getstream_ruby/generated/models/feed_group_restored_event.rb b/lib/getstream_ruby/generated/models/feed_group_restored_event.rb new file mode 100644 index 0000000..821064a --- /dev/null +++ b/lib/getstream_ruby/generated/models/feed_group_restored_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a feed group is restored. + class FeedGroupRestoredEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute fid + # @return [String] + attr_accessor :fid + # @!attribute group_id + # @return [String] The ID of the feed group that was restored + attr_accessor :group_id + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "feeds.feed_group.restored" in this case + attr_accessor :type + # @!attribute feed_visibility + # @return [String] + attr_accessor :feed_visibility + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @fid = attributes[:fid] || attributes['fid'] + @group_id = attributes[:group_id] || attributes['group_id'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "feeds.feed_group.restored" + @feed_visibility = attributes[:feed_visibility] || attributes['feed_visibility'] || nil + @received_at = attributes[:received_at] || attributes['received_at'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + fid: 'fid', + group_id: 'group_id', + custom: 'custom', + type: 'type', + feed_visibility: 'feed_visibility', + received_at: 'received_at' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/get_user_group_response.rb b/lib/getstream_ruby/generated/models/get_user_group_response.rb new file mode 100644 index 0000000..a41e20a --- /dev/null +++ b/lib/getstream_ruby/generated/models/get_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for getting a user group + class GetUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/import_v2_task_settings.rb b/lib/getstream_ruby/generated/models/import_v2_task_settings.rb index eccd5f0..30f7f79 100644 --- a/lib/getstream_ruby/generated/models/import_v2_task_settings.rb +++ b/lib/getstream_ruby/generated/models/import_v2_task_settings.rb @@ -9,6 +9,9 @@ module Models class ImportV2TaskSettings < GetStream::BaseModel # Model attributes + # @!attribute merge_custom + # @return [Boolean] + attr_accessor :merge_custom # @!attribute mode # @return [String] attr_accessor :mode @@ -25,6 +28,7 @@ class ImportV2TaskSettings < GetStream::BaseModel # Initialize with attributes def initialize(attributes = {}) super(attributes) + @merge_custom = attributes[:merge_custom] || attributes['merge_custom'] || nil @mode = attributes[:mode] || attributes['mode'] || nil @path = attributes[:path] || attributes['path'] || nil @skip_references_check = attributes[:skip_references_check] || attributes['skip_references_check'] || nil @@ -34,6 +38,7 @@ def initialize(attributes = {}) # Override field mappings for JSON serialization def self.json_field_mappings { + merge_custom: 'merge_custom', mode: 'mode', path: 'path', skip_references_check: 'skip_references_check', diff --git a/lib/getstream_ruby/generated/models/individual_record_settings.rb b/lib/getstream_ruby/generated/models/individual_record_settings.rb index 63c0bcf..f5fe2e5 100644 --- a/lib/getstream_ruby/generated/models/individual_record_settings.rb +++ b/lib/getstream_ruby/generated/models/individual_record_settings.rb @@ -12,17 +12,22 @@ class IndividualRecordSettings < GetStream::BaseModel # @!attribute mode # @return [String] attr_accessor :mode + # @!attribute output_types + # @return [Array] + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb b/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb index cba368a..1e3c8c4 100644 --- a/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb +++ b/lib/getstream_ruby/generated/models/individual_recording_settings_request.rb @@ -12,17 +12,22 @@ class IndividualRecordingSettingsRequest < GetStream::BaseModel # @!attribute mode # @return [String] Recording mode. One of: available, disabled, auto-on attr_accessor :mode + # @!attribute output_types + # @return [Array] Output types to include: audio_only, video_only, audio_video, screenshare_audio_only, screenshare_video_only, screenshare_audio_video + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb b/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb index ab57f2a..f72f920 100644 --- a/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb +++ b/lib/getstream_ruby/generated/models/individual_recording_settings_response.rb @@ -12,17 +12,22 @@ class IndividualRecordingSettingsResponse < GetStream::BaseModel # @!attribute mode # @return [String] attr_accessor :mode + # @!attribute output_types + # @return [Array] + attr_accessor :output_types # Initialize with attributes def initialize(attributes = {}) super(attributes) @mode = attributes[:mode] || attributes['mode'] + @output_types = attributes[:output_types] || attributes['output_types'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { - mode: 'mode' + mode: 'mode', + output_types: 'output_types' } end end diff --git a/lib/getstream_ruby/generated/models/list_user_groups_response.rb b/lib/getstream_ruby/generated/models/list_user_groups_response.rb new file mode 100644 index 0000000..1c79862 --- /dev/null +++ b/lib/getstream_ruby/generated/models/list_user_groups_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for listing user groups + class ListUserGroupsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_groups + # @return [Array] List of user groups + attr_accessor :user_groups + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_groups = attributes[:user_groups] || attributes['user_groups'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_groups: 'user_groups' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/message_request.rb b/lib/getstream_ruby/generated/models/message_request.rb index f2fb16d..a17fb05 100644 --- a/lib/getstream_ruby/generated/models/message_request.rb +++ b/lib/getstream_ruby/generated/models/message_request.rb @@ -60,6 +60,9 @@ class MessageRequest < GetStream::BaseModel # @!attribute attachments # @return [Array] Array of message attachments attr_accessor :attachments + # @!attribute mentioned_roles + # @return [Array] + attr_accessor :mentioned_roles # @!attribute mentioned_users # @return [Array] Array of user IDs to mention attr_accessor :mentioned_users @@ -96,6 +99,7 @@ def initialize(attributes = {}) @type = attributes[:type] || attributes['type'] || nil @user_id = attributes[:user_id] || attributes['user_id'] || nil @attachments = attributes[:attachments] || attributes['attachments'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @mentioned_users = attributes[:mentioned_users] || attributes['mentioned_users'] || nil @restricted_visibility = attributes[:restricted_visibility] || attributes['restricted_visibility'] || nil @custom = attributes[:custom] || attributes['custom'] || nil @@ -123,6 +127,7 @@ def self.json_field_mappings type: 'type', user_id: 'user_id', attachments: 'attachments', + mentioned_roles: 'mentioned_roles', mentioned_users: 'mentioned_users', restricted_visibility: 'restricted_visibility', custom: 'custom', diff --git a/lib/getstream_ruby/generated/models/message_response.rb b/lib/getstream_ruby/generated/models/message_response.rb index a49ba4d..fd34cb4 100644 --- a/lib/getstream_ruby/generated/models/message_response.rb +++ b/lib/getstream_ruby/generated/models/message_response.rb @@ -111,6 +111,9 @@ class MessageResponse < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] Whether thread reply should be shown in the channel as well attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] List of roles mentioned in the message (e.g. admin, channel_moderator, custom roles). Members with matching roles will receive push notifications based on their push preferences. Max 10 roles + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] List of users who participate in thread attr_accessor :thread_participants @@ -185,6 +188,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @i18n = attributes[:i18n] || attributes['i18n'] || nil @@ -236,6 +240,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', draft: 'draft', i18n: 'i18n', diff --git a/lib/getstream_ruby/generated/models/message_with_channel_response.rb b/lib/getstream_ruby/generated/models/message_with_channel_response.rb index 99934f0..5400b70 100644 --- a/lib/getstream_ruby/generated/models/message_with_channel_response.rb +++ b/lib/getstream_ruby/generated/models/message_with_channel_response.rb @@ -114,6 +114,9 @@ class MessageWithChannelResponse < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] Whether thread reply should be shown in the channel as well attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] List of roles mentioned in the message (e.g. admin, channel_moderator, custom roles). Members with matching roles will receive push notifications based on their push preferences. Max 10 roles + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] List of users who participate in thread attr_accessor :thread_participants @@ -189,6 +192,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @i18n = attributes[:i18n] || attributes['i18n'] || nil @@ -241,6 +245,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', draft: 'draft', i18n: 'i18n', diff --git a/lib/getstream_ruby/generated/models/metric_stats.rb b/lib/getstream_ruby/generated/models/metric_stats.rb new file mode 100644 index 0000000..ef557c3 --- /dev/null +++ b/lib/getstream_ruby/generated/models/metric_stats.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Statistics for a single metric with optional daily breakdown + class MetricStats < GetStream::BaseModel + + # Model attributes + # @!attribute total + # @return [Integer] Aggregated total value + attr_accessor :total + # @!attribute daily + # @return [Array] Per-day values (only present in daily mode) + attr_accessor :daily + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @total = attributes[:total] || attributes['total'] + @daily = attributes[:daily] || attributes['daily'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + total: 'total', + daily: 'daily' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb b/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb new file mode 100644 index 0000000..1a008c0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/query_team_usage_stats_request.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request payload for querying team-level usage statistics from the warehouse database + class QueryTeamUsageStatsRequest < GetStream::BaseModel + + # Model attributes + # @!attribute end_date + # @return [String] End date in YYYY-MM-DD format. Used with start_date for custom date range. Returns daily breakdown. + attr_accessor :end_date + # @!attribute limit + # @return [Integer] Maximum number of teams to return per page (default: 30, max: 30) + attr_accessor :limit + # @!attribute month + # @return [String] Month in YYYY-MM format (e.g., '2026-01'). Mutually exclusive with start_date/end_date. Returns aggregated monthly values. + attr_accessor :month + # @!attribute next + # @return [String] Cursor for pagination to fetch next page of teams + attr_accessor :next + # @!attribute start_date + # @return [String] Start date in YYYY-MM-DD format. Used with end_date for custom date range. Returns daily breakdown. + attr_accessor :start_date + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @end_date = attributes[:end_date] || attributes['end_date'] || nil + @limit = attributes[:limit] || attributes['limit'] || nil + @month = attributes[:month] || attributes['month'] || nil + @next = attributes[:next] || attributes['next'] || nil + @start_date = attributes[:start_date] || attributes['start_date'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + end_date: 'end_date', + limit: 'limit', + month: 'month', + next: 'next', + start_date: 'start_date' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb b/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb new file mode 100644 index 0000000..377cbec --- /dev/null +++ b/lib/getstream_ruby/generated/models/query_team_usage_stats_response.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response containing team-level usage statistics + class QueryTeamUsageStatsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] Duration of the request in milliseconds + attr_accessor :duration + # @!attribute teams + # @return [Array] Array of team usage statistics + attr_accessor :teams + # @!attribute next + # @return [String] Cursor for pagination to fetch next page + attr_accessor :next + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @teams = attributes[:teams] || attributes['teams'] + @next = attributes[:next] || attributes['next'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + teams: 'teams', + next: 'next' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/read_collections_response.rb b/lib/getstream_ruby/generated/models/read_collections_response.rb index 2845c58..89f1b12 100644 --- a/lib/getstream_ruby/generated/models/read_collections_response.rb +++ b/lib/getstream_ruby/generated/models/read_collections_response.rb @@ -15,19 +15,29 @@ class ReadCollectionsResponse < GetStream::BaseModel # @!attribute collections # @return [Array] List of collections matching the query attr_accessor :collections + # @!attribute next + # @return [String] Cursor for next page (when listing without collection_refs) + attr_accessor :next + # @!attribute prev + # @return [String] Cursor for previous page (when listing without collection_refs) + attr_accessor :prev # Initialize with attributes def initialize(attributes = {}) super(attributes) @duration = attributes[:duration] || attributes['duration'] @collections = attributes[:collections] || attributes['collections'] + @next = attributes[:next] || attributes['next'] || nil + @prev = attributes[:prev] || attributes['prev'] || nil end # Override field mappings for JSON serialization def self.json_field_mappings { duration: 'duration', - collections: 'collections' + collections: 'collections', + next: 'next', + prev: 'prev' } end end diff --git a/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb b/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb new file mode 100644 index 0000000..85be53d --- /dev/null +++ b/lib/getstream_ruby/generated/models/remove_user_group_members_request.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for removing members from a user group + class RemoveUserGroupMembersRequest < GetStream::BaseModel + + # Model attributes + # @!attribute member_ids + # @return [Array] List of user IDs to remove from the group + attr_accessor :member_ids + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @member_ids = attributes[:member_ids] || attributes['member_ids'] + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + member_ids: 'member_ids', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb b/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb new file mode 100644 index 0000000..c78add5 --- /dev/null +++ b/lib/getstream_ruby/generated/models/remove_user_group_members_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for removing members from a user group + class RemoveUserGroupMembersResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/restore_feed_group_request.rb b/lib/getstream_ruby/generated/models/restore_feed_group_request.rb new file mode 100644 index 0000000..55581de --- /dev/null +++ b/lib/getstream_ruby/generated/models/restore_feed_group_request.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class RestoreFeedGroupRequest < GetStream::BaseModel + # Empty model - inherits all functionality from BaseModel + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/restore_feed_group_response.rb b/lib/getstream_ruby/generated/models/restore_feed_group_response.rb new file mode 100644 index 0000000..0d82a0e --- /dev/null +++ b/lib/getstream_ruby/generated/models/restore_feed_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class RestoreFeedGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute feed_group + # @return [FeedGroupResponse] + attr_accessor :feed_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @feed_group = attributes[:feed_group] || attributes['feed_group'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + feed_group: 'feed_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/rule_builder_condition.rb b/lib/getstream_ruby/generated/models/rule_builder_condition.rb index f7b5ed2..16d4739 100644 --- a/lib/getstream_ruby/generated/models/rule_builder_condition.rb +++ b/lib/getstream_ruby/generated/models/rule_builder_condition.rb @@ -21,6 +21,9 @@ class RuleBuilderCondition < GetStream::BaseModel # @!attribute call_type_rule_params # @return [CallTypeRuleParameters] attr_accessor :call_type_rule_params + # @!attribute call_violation_count_params + # @return [CallViolationCountParameters] + attr_accessor :call_violation_count_params # @!attribute closed_caption_rule_params # @return [ClosedCaptionRuleParameters] attr_accessor :closed_caption_rule_params @@ -77,6 +80,7 @@ def initialize(attributes = {}) @type = attributes[:type] || attributes['type'] || nil @call_custom_property_params = attributes[:call_custom_property_params] || attributes['call_custom_property_params'] || nil @call_type_rule_params = attributes[:call_type_rule_params] || attributes['call_type_rule_params'] || nil + @call_violation_count_params = attributes[:call_violation_count_params] || attributes['call_violation_count_params'] || nil @closed_caption_rule_params = attributes[:closed_caption_rule_params] || attributes['closed_caption_rule_params'] || nil @content_count_rule_params = attributes[:content_count_rule_params] || attributes['content_count_rule_params'] || nil @content_flag_count_rule_params = attributes[:content_flag_count_rule_params] || attributes['content_flag_count_rule_params'] || nil @@ -102,6 +106,7 @@ def self.json_field_mappings type: 'type', call_custom_property_params: 'call_custom_property_params', call_type_rule_params: 'call_type_rule_params', + call_violation_count_params: 'call_violation_count_params', closed_caption_rule_params: 'closed_caption_rule_params', content_count_rule_params: 'content_count_rule_params', content_flag_count_rule_params: 'content_flag_count_rule_params', diff --git a/lib/getstream_ruby/generated/models/search_result_message.rb b/lib/getstream_ruby/generated/models/search_result_message.rb index 348034f..c54004c 100644 --- a/lib/getstream_ruby/generated/models/search_result_message.rb +++ b/lib/getstream_ruby/generated/models/search_result_message.rb @@ -111,6 +111,9 @@ class SearchResultMessage < GetStream::BaseModel # @!attribute show_in_channel # @return [Boolean] attr_accessor :show_in_channel + # @!attribute mentioned_roles + # @return [Array] + attr_accessor :mentioned_roles # @!attribute thread_participants # @return [Array] attr_accessor :thread_participants @@ -188,6 +191,7 @@ def initialize(attributes = {}) @poll_id = attributes[:poll_id] || attributes['poll_id'] || nil @quoted_message_id = attributes[:quoted_message_id] || attributes['quoted_message_id'] || nil @show_in_channel = attributes[:show_in_channel] || attributes['show_in_channel'] || nil + @mentioned_roles = attributes[:mentioned_roles] || attributes['mentioned_roles'] || nil @thread_participants = attributes[:thread_participants] || attributes['thread_participants'] || nil @channel = attributes[:channel] || attributes['channel'] || nil @draft = attributes[:draft] || attributes['draft'] || nil @@ -240,6 +244,7 @@ def self.json_field_mappings poll_id: 'poll_id', quoted_message_id: 'quoted_message_id', show_in_channel: 'show_in_channel', + mentioned_roles: 'mentioned_roles', thread_participants: 'thread_participants', channel: 'channel', draft: 'draft', diff --git a/lib/getstream_ruby/generated/models/search_user_groups_response.rb b/lib/getstream_ruby/generated/models/search_user_groups_response.rb new file mode 100644 index 0000000..424a14d --- /dev/null +++ b/lib/getstream_ruby/generated/models/search_user_groups_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for searching user groups + class SearchUserGroupsResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_groups + # @return [Array] List of matching user groups + attr_accessor :user_groups + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_groups = attributes[:user_groups] || attributes['user_groups'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_groups: 'user_groups' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/team_usage_stats.rb b/lib/getstream_ruby/generated/models/team_usage_stats.rb new file mode 100644 index 0000000..0b8b25b --- /dev/null +++ b/lib/getstream_ruby/generated/models/team_usage_stats.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Usage statistics for a single team containing all 16 metrics + class TeamUsageStats < GetStream::BaseModel + + # Model attributes + # @!attribute team + # @return [String] Team identifier (empty string for users not assigned to any team) + attr_accessor :team + # @!attribute concurrent_connections + # @return [MetricStats] + attr_accessor :concurrent_connections + # @!attribute concurrent_users + # @return [MetricStats] + attr_accessor :concurrent_users + # @!attribute image_moderations_daily + # @return [MetricStats] + attr_accessor :image_moderations_daily + # @!attribute messages_daily + # @return [MetricStats] + attr_accessor :messages_daily + # @!attribute messages_last_24_hours + # @return [MetricStats] + attr_accessor :messages_last_24_hours + # @!attribute messages_last_30_days + # @return [MetricStats] + attr_accessor :messages_last_30_days + # @!attribute messages_month_to_date + # @return [MetricStats] + attr_accessor :messages_month_to_date + # @!attribute messages_total + # @return [MetricStats] + attr_accessor :messages_total + # @!attribute translations_daily + # @return [MetricStats] + attr_accessor :translations_daily + # @!attribute users_daily + # @return [MetricStats] + attr_accessor :users_daily + # @!attribute users_engaged_last_30_days + # @return [MetricStats] + attr_accessor :users_engaged_last_30_days + # @!attribute users_engaged_month_to_date + # @return [MetricStats] + attr_accessor :users_engaged_month_to_date + # @!attribute users_last_24_hours + # @return [MetricStats] + attr_accessor :users_last_24_hours + # @!attribute users_last_30_days + # @return [MetricStats] + attr_accessor :users_last_30_days + # @!attribute users_month_to_date + # @return [MetricStats] + attr_accessor :users_month_to_date + # @!attribute users_total + # @return [MetricStats] + attr_accessor :users_total + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @team = attributes[:team] || attributes['team'] + @concurrent_connections = attributes[:concurrent_connections] || attributes['concurrent_connections'] + @concurrent_users = attributes[:concurrent_users] || attributes['concurrent_users'] + @image_moderations_daily = attributes[:image_moderations_daily] || attributes['image_moderations_daily'] + @messages_daily = attributes[:messages_daily] || attributes['messages_daily'] + @messages_last_24_hours = attributes[:messages_last_24_hours] || attributes['messages_last_24_hours'] + @messages_last_30_days = attributes[:messages_last_30_days] || attributes['messages_last_30_days'] + @messages_month_to_date = attributes[:messages_month_to_date] || attributes['messages_month_to_date'] + @messages_total = attributes[:messages_total] || attributes['messages_total'] + @translations_daily = attributes[:translations_daily] || attributes['translations_daily'] + @users_daily = attributes[:users_daily] || attributes['users_daily'] + @users_engaged_last_30_days = attributes[:users_engaged_last_30_days] || attributes['users_engaged_last_30_days'] + @users_engaged_month_to_date = attributes[:users_engaged_month_to_date] || attributes['users_engaged_month_to_date'] + @users_last_24_hours = attributes[:users_last_24_hours] || attributes['users_last_24_hours'] + @users_last_30_days = attributes[:users_last_30_days] || attributes['users_last_30_days'] + @users_month_to_date = attributes[:users_month_to_date] || attributes['users_month_to_date'] + @users_total = attributes[:users_total] || attributes['users_total'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + team: 'team', + concurrent_connections: 'concurrent_connections', + concurrent_users: 'concurrent_users', + image_moderations_daily: 'image_moderations_daily', + messages_daily: 'messages_daily', + messages_last_24_hours: 'messages_last_24_hours', + messages_last_30_days: 'messages_last_30_days', + messages_month_to_date: 'messages_month_to_date', + messages_total: 'messages_total', + translations_daily: 'translations_daily', + users_daily: 'users_daily', + users_engaged_last_30_days: 'users_engaged_last_30_days', + users_engaged_month_to_date: 'users_engaged_month_to_date', + users_last_24_hours: 'users_last_24_hours', + users_last_30_days: 'users_last_30_days', + users_month_to_date: 'users_month_to_date', + users_total: 'users_total' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/undelete_message_request.rb b/lib/getstream_ruby/generated/models/undelete_message_request.rb new file mode 100644 index 0000000..bf746b1 --- /dev/null +++ b/lib/getstream_ruby/generated/models/undelete_message_request.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UndeleteMessageRequest < GetStream::BaseModel + + # Model attributes + # @!attribute undeleted_by + # @return [String] ID of the user who is undeleting the message + attr_accessor :undeleted_by + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @undeleted_by = attributes[:undeleted_by] || attributes['undeleted_by'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + undeleted_by: 'undeleted_by' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/undelete_message_response.rb b/lib/getstream_ruby/generated/models/undelete_message_response.rb new file mode 100644 index 0000000..ab348cb --- /dev/null +++ b/lib/getstream_ruby/generated/models/undelete_message_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Basic response information + class UndeleteMessageResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] Duration of the request in milliseconds + attr_accessor :duration + # @!attribute message + # @return [MessageResponse] + attr_accessor :message + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @message = attributes[:message] || attributes['message'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + message: 'message' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/update_app_request.rb b/lib/getstream_ruby/generated/models/update_app_request.rb index 6644078..fce54e5 100644 --- a/lib/getstream_ruby/generated/models/update_app_request.rb +++ b/lib/getstream_ruby/generated/models/update_app_request.rb @@ -54,6 +54,9 @@ class UpdateAppRequest < GetStream::BaseModel # @!attribute migrate_permissions_to_v2 # @return [Boolean] attr_accessor :migrate_permissions_to_v2 + # @!attribute moderation_analytics_enabled + # @return [Boolean] + attr_accessor :moderation_analytics_enabled # @!attribute moderation_enabled # @return [Boolean] attr_accessor :moderation_enabled @@ -169,6 +172,7 @@ def initialize(attributes = {}) @image_moderation_enabled = attributes[:image_moderation_enabled] || attributes['image_moderation_enabled'] || nil @max_aggregated_activities_length = attributes[:max_aggregated_activities_length] || attributes['max_aggregated_activities_length'] || nil @migrate_permissions_to_v2 = attributes[:migrate_permissions_to_v2] || attributes['migrate_permissions_to_v2'] || nil + @moderation_analytics_enabled = attributes[:moderation_analytics_enabled] || attributes['moderation_analytics_enabled'] || nil @moderation_enabled = attributes[:moderation_enabled] || attributes['moderation_enabled'] || nil @moderation_webhook_url = attributes[:moderation_webhook_url] || attributes['moderation_webhook_url'] || nil @multi_tenant_enabled = attributes[:multi_tenant_enabled] || attributes['multi_tenant_enabled'] || nil @@ -221,6 +225,7 @@ def self.json_field_mappings image_moderation_enabled: 'image_moderation_enabled', max_aggregated_activities_length: 'max_aggregated_activities_length', migrate_permissions_to_v2: 'migrate_permissions_to_v2', + moderation_analytics_enabled: 'moderation_analytics_enabled', moderation_enabled: 'moderation_enabled', moderation_webhook_url: 'moderation_webhook_url', multi_tenant_enabled: 'multi_tenant_enabled', diff --git a/lib/getstream_ruby/generated/models/update_user_group_request.rb b/lib/getstream_ruby/generated/models/update_user_group_request.rb new file mode 100644 index 0000000..793c3e0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/update_user_group_request.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Request body for updating a user group + class UpdateUserGroupRequest < GetStream::BaseModel + + # Model attributes + # @!attribute description + # @return [String] The new description for the group + attr_accessor :description + # @!attribute name + # @return [String] The new name of the user group + attr_accessor :name + # @!attribute team_id + # @return [String] + attr_accessor :team_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @description = attributes[:description] || attributes['description'] || nil + @name = attributes[:name] || attributes['name'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + description: 'description', + name: 'name', + team_id: 'team_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/update_user_group_response.rb b/lib/getstream_ruby/generated/models/update_user_group_response.rb new file mode 100644 index 0000000..9e98bbf --- /dev/null +++ b/lib/getstream_ruby/generated/models/update_user_group_response.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Response for updating a user group + class UpdateUserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute duration + # @return [String] + attr_accessor :duration + # @!attribute user_group + # @return [UserGroupResponse] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @duration = attributes[:duration] || attributes['duration'] + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + duration: 'duration', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group.rb b/lib/getstream_ruby/generated/models/user_group.rb new file mode 100644 index 0000000..6328ecd --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroup < GetStream::BaseModel + + # Model attributes + # @!attribute app_pk + # @return [Integer] + attr_accessor :app_pk + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute id + # @return [String] + attr_accessor :id + # @!attribute name + # @return [String] + attr_accessor :name + # @!attribute updated_at + # @return [DateTime] + attr_accessor :updated_at + # @!attribute created_by + # @return [String] + attr_accessor :created_by + # @!attribute description + # @return [String] + attr_accessor :description + # @!attribute team_id + # @return [String] + attr_accessor :team_id + # @!attribute members + # @return [Array] + attr_accessor :members + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @app_pk = attributes[:app_pk] || attributes['app_pk'] + @created_at = attributes[:created_at] || attributes['created_at'] + @id = attributes[:id] || attributes['id'] + @name = attributes[:name] || attributes['name'] + @updated_at = attributes[:updated_at] || attributes['updated_at'] + @created_by = attributes[:created_by] || attributes['created_by'] || nil + @description = attributes[:description] || attributes['description'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @members = attributes[:members] || attributes['members'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + app_pk: 'app_pk', + created_at: 'created_at', + id: 'id', + name: 'name', + updated_at: 'updated_at', + created_by: 'created_by', + description: 'description', + team_id: 'team_id', + members: 'members' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_created_event.rb b/lib/getstream_ruby/generated/models/user_group_created_event.rb new file mode 100644 index 0000000..cbe7234 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_created_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is created. + class UserGroupCreatedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.created" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.created" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_deleted_event.rb b/lib/getstream_ruby/generated/models/user_group_deleted_event.rb new file mode 100644 index 0000000..b09fc27 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_deleted_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is deleted. + class UserGroupDeletedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.deleted" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.deleted" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member.rb b/lib/getstream_ruby/generated/models/user_group_member.rb new file mode 100644 index 0000000..75aef41 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroupMember < GetStream::BaseModel + + # Model attributes + # @!attribute app_pk + # @return [Integer] + attr_accessor :app_pk + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute group_id + # @return [String] + attr_accessor :group_id + # @!attribute is_admin + # @return [Boolean] + attr_accessor :is_admin + # @!attribute user_id + # @return [String] + attr_accessor :user_id + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @app_pk = attributes[:app_pk] || attributes['app_pk'] + @created_at = attributes[:created_at] || attributes['created_at'] + @group_id = attributes[:group_id] || attributes['group_id'] + @is_admin = attributes[:is_admin] || attributes['is_admin'] + @user_id = attributes[:user_id] || attributes['user_id'] + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + app_pk: 'app_pk', + created_at: 'created_at', + group_id: 'group_id', + is_admin: 'is_admin', + user_id: 'user_id' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member_added_event.rb b/lib/getstream_ruby/generated/models/user_group_member_added_event.rb new file mode 100644 index 0000000..7efeccd --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member_added_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when members are added to a user group. + class UserGroupMemberAddedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute members + # @return [Array] The user IDs that were added + attr_accessor :members + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.member_added" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @members = attributes[:members] || attributes['members'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.member_added" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + members: 'members', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb b/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb new file mode 100644 index 0000000..c6edf41 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_member_removed_event.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when members are removed from a user group. + class UserGroupMemberRemovedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute members + # @return [Array] The user IDs that were removed + attr_accessor :members + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.member_removed" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @members = attributes[:members] || attributes['members'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.member_removed" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + members: 'members', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_response.rb b/lib/getstream_ruby/generated/models/user_group_response.rb new file mode 100644 index 0000000..2d7d8c0 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_response.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # + class UserGroupResponse < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] + attr_accessor :created_at + # @!attribute id + # @return [String] + attr_accessor :id + # @!attribute name + # @return [String] + attr_accessor :name + # @!attribute updated_at + # @return [DateTime] + attr_accessor :updated_at + # @!attribute created_by + # @return [String] + attr_accessor :created_by + # @!attribute description + # @return [String] + attr_accessor :description + # @!attribute team_id + # @return [String] + attr_accessor :team_id + # @!attribute members + # @return [Array] + attr_accessor :members + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @id = attributes[:id] || attributes['id'] + @name = attributes[:name] || attributes['name'] + @updated_at = attributes[:updated_at] || attributes['updated_at'] + @created_by = attributes[:created_by] || attributes['created_by'] || nil + @description = attributes[:description] || attributes['description'] || nil + @team_id = attributes[:team_id] || attributes['team_id'] || nil + @members = attributes[:members] || attributes['members'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + id: 'id', + name: 'name', + updated_at: 'updated_at', + created_by: 'created_by', + description: 'description', + team_id: 'team_id', + members: 'members' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/models/user_group_updated_event.rb b/lib/getstream_ruby/generated/models/user_group_updated_event.rb new file mode 100644 index 0000000..3ebb5c2 --- /dev/null +++ b/lib/getstream_ruby/generated/models/user_group_updated_event.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Code generated by GetStream internal OpenAPI code generator. DO NOT EDIT. + +module GetStream + module Generated + module Models + # Emitted when a user group is updated. + class UserGroupUpdatedEvent < GetStream::BaseModel + + # Model attributes + # @!attribute created_at + # @return [DateTime] Date/time of creation + attr_accessor :created_at + # @!attribute custom + # @return [Object] + attr_accessor :custom + # @!attribute type + # @return [String] The type of event: "user_group.updated" in this case + attr_accessor :type + # @!attribute received_at + # @return [DateTime] + attr_accessor :received_at + # @!attribute user + # @return [UserResponseCommonFields] + attr_accessor :user + # @!attribute user_group + # @return [UserGroup] + attr_accessor :user_group + + # Initialize with attributes + def initialize(attributes = {}) + super(attributes) + @created_at = attributes[:created_at] || attributes['created_at'] + @custom = attributes[:custom] || attributes['custom'] + @type = attributes[:type] || attributes['type'] || "user_group.updated" + @received_at = attributes[:received_at] || attributes['received_at'] || nil + @user = attributes[:user] || attributes['user'] || nil + @user_group = attributes[:user_group] || attributes['user_group'] || nil + end + + # Override field mappings for JSON serialization + def self.json_field_mappings + { + created_at: 'created_at', + custom: 'custom', + type: 'type', + received_at: 'received_at', + user: 'user', + user_group: 'user_group' + } + end + end + end + end +end diff --git a/lib/getstream_ruby/generated/webhook.rb b/lib/getstream_ruby/generated/webhook.rb index 017a64e..266f1f4 100644 --- a/lib/getstream_ruby/generated/webhook.rb +++ b/lib/getstream_ruby/generated/webhook.rb @@ -102,6 +102,7 @@ require_relative 'models/feed_deleted_event' require_relative 'models/feed_group_changed_event' require_relative 'models/feed_group_deleted_event' +require_relative 'models/feed_group_restored_event' require_relative 'models/feed_member_added_event' require_relative 'models/feed_member_removed_event' require_relative 'models/feed_member_updated_event' @@ -152,6 +153,11 @@ require_relative 'models/user_deactivated_event' require_relative 'models/user_deleted_event' require_relative 'models/user_flagged_event' +require_relative 'models/user_group_created_event' +require_relative 'models/user_group_deleted_event' +require_relative 'models/user_group_member_added_event' +require_relative 'models/user_group_member_removed_event' +require_relative 'models/user_group_updated_event' require_relative 'models/user_messages_deleted_event' require_relative 'models/user_muted_event' require_relative 'models/user_reactivated_event' @@ -272,6 +278,7 @@ module Webhook EVENT_TYPE_FEEDS_FEED_UPDATED = 'feeds.feed.updated' EVENT_TYPE_FEEDS_FEED_GROUP_CHANGED = 'feeds.feed_group.changed' EVENT_TYPE_FEEDS_FEED_GROUP_DELETED = 'feeds.feed_group.deleted' + EVENT_TYPE_FEEDS_FEED_GROUP_RESTORED = 'feeds.feed_group.restored' EVENT_TYPE_FEEDS_FEED_MEMBER_ADDED = 'feeds.feed_member.added' EVENT_TYPE_FEEDS_FEED_MEMBER_REMOVED = 'feeds.feed_member.removed' EVENT_TYPE_FEEDS_FEED_MEMBER_UPDATED = 'feeds.feed_member.updated' @@ -323,6 +330,11 @@ module Webhook EVENT_TYPE_USER_UNMUTED = 'user.unmuted' EVENT_TYPE_USER_UNREAD_MESSAGE_REMINDER = 'user.unread_message_reminder' EVENT_TYPE_USER_UPDATED = 'user.updated' + EVENT_TYPE_USER_GROUP_CREATED = 'user_group.created' + EVENT_TYPE_USER_GROUP_DELETED = 'user_group.deleted' + EVENT_TYPE_USER_GROUP_MEMBER_ADDED = 'user_group.member_added' + EVENT_TYPE_USER_GROUP_MEMBER_REMOVED = 'user_group.member_removed' + EVENT_TYPE_USER_GROUP_UPDATED = 'user_group.updated' # Extract the event type from a raw webhook payload. # @@ -586,6 +598,8 @@ def self.parse_webhook_event(raw_event) StreamChat::FeedGroupChangedEvent when 'feeds.feed_group.deleted' StreamChat::FeedGroupDeletedEvent + when 'feeds.feed_group.restored' + StreamChat::FeedGroupRestoredEvent when 'feeds.feed_member.added' StreamChat::FeedMemberAddedEvent when 'feeds.feed_member.removed' @@ -688,6 +702,16 @@ def self.parse_webhook_event(raw_event) StreamChat::UserUnreadReminderEvent when 'user.updated' StreamChat::UserUpdatedEvent + when 'user_group.created' + StreamChat::UserGroupCreatedEvent + when 'user_group.deleted' + StreamChat::UserGroupDeletedEvent + when 'user_group.member_added' + StreamChat::UserGroupMemberAddedEvent + when 'user_group.member_removed' + StreamChat::UserGroupMemberRemovedEvent + when 'user_group.updated' + StreamChat::UserGroupUpdatedEvent else nil end diff --git a/spec/integration/base_integration_test.rb b/spec/integration/base_integration_test.rb index cca3e90..90efac3 100644 --- a/spec/integration/base_integration_test.rb +++ b/spec/integration/base_integration_test.rb @@ -4,6 +4,7 @@ require 'securerandom' require 'dotenv' require_relative '../../lib/getstream_ruby' +require_relative 'suite_cleanup' # Base class for integration tests with common setup and cleanup class BaseIntegrationTest diff --git a/spec/integration/chat_channel_integration_spec.rb b/spec/integration/chat_channel_integration_spec.rb new file mode 100644 index 0000000..e37574a --- /dev/null +++ b/spec/integration/chat_channel_integration_spec.rb @@ -0,0 +1,953 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require 'tempfile' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Channel Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + # Create shared test users for all subtests + @shared_user_ids, _resp = create_test_users(4) + @creator_id = @shared_user_ids[0] + @member_id_1 = @shared_user_ids[1] + @member_id_2 = @shared_user_ids[2] + @member_id_3 = @shared_user_ids[3] + + end + + after(:all) do + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Channel API wrappers (beyond what ChatTestHelpers provides) + # --------------------------------------------------------------------------- + + def update_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}", body: body) + end + + def update_channel_partial(type, id, body) + @client.make_request(:patch, "/api/v2/chat/channels/#{type}/#{id}", body: body) + end + + def delete_channels_batch(body) + @client.make_request(:post, '/api/v2/chat/channels/delete', body: body) + end + + def hide_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/hide", body: body) + end + + def show_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/show", body: body) + end + + def truncate_channel(type, id, body = {}) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/truncate", body: body) + end + + def mark_read(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/read", body: body) + end + + def mark_unread(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/unread", body: body) + end + + def send_event(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/event", body: body) + end + + def mute_channel(body) + @client.make_request(:post, '/api/v2/chat/moderation/mute/channel', body: body) + end + + def unmute_channel(body) + @client.make_request(:post, '/api/v2/chat/moderation/unmute/channel', body: body) + end + + def update_member_partial(type, id, body) + user_id = body.delete(:user_id) || body.delete('user_id') + @client.make_request( + :patch, + "/api/v2/chat/channels/#{type}/#{id}/member", + query_params: { 'user_id' => user_id }, + body: body, + ) + end + + def query_members_api(payload) + @client.make_request( + :get, + '/api/v2/chat/members', + query_params: { 'payload' => JSON.generate(payload) }, + ) + end + + def upload_channel_file(type, id, file_upload_request) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/file", body: file_upload_request) + end + + def delete_channel_file(type, id, url) + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}/file", query_params: { 'url' => url }) + end + + def upload_channel_image(type, id, image_upload_request) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/image", body: image_upload_request) + end + + def delete_channel_image(type, id, url) + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}/image", query_params: { 'url' => url }) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateChannelWithID' do + + it 'creates channel and verifies via QueryChannels' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = query_channels( + filter_conditions: { 'id' => channel_id }, + ) + expect(resp.channels).not_to be_nil + expect(resp.channels).not_to be_empty + ch = resp.channels.first.to_h + expect(ch.dig('channel', 'id')).to eq(channel_id) + expect(ch.dig('channel', 'type')).to eq('messaging') + + end + + end + + describe 'CreateChannelWithMembers' do + + it 'creates channel with 3 members and verifies count' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, + [@creator_id, @member_id_1, @member_id_2], + ) + + resp = get_or_create_channel('messaging', channel_id) + expect(resp.members).not_to be_nil + expect(resp.members.length).to be >= 3 + + end + + end + + describe 'CreateDistinctChannel' do + + it 'creates distinct channel and verifies same CID on second call' do + + members = [ + { user_id: @creator_id }, + { user_id: @member_id_1 }, + ] + + resp = @client.make_request( + :post, + '/api/v2/chat/channels/messaging/query', + body: { + data: { + created_by_id: @creator_id, + members: members, + }, + }, + ) + expect(resp.channel).not_to be_nil + cid_1 = resp.channel.to_h['cid'] + + # Call again with same members — should return same channel + resp_2 = @client.make_request( + :post, + '/api/v2/chat/channels/messaging/query', + body: { + data: { + created_by_id: @creator_id, + members: members, + }, + }, + ) + cid_2 = resp_2.channel.to_h['cid'] + expect(cid_1).to eq(cid_2) + + # Cleanup: hard delete + ch_id = resp.channel.to_h['id'] + @created_channel_cids << "messaging:#{ch_id}" unless @created_channel_cids.include?("messaging:#{ch_id}") + + end + + end + + describe 'QueryChannels' do + + it 'creates channel and queries by type+id' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = query_channels( + filter_conditions: { 'type' => 'messaging', 'id' => channel_id }, + ) + expect(resp.channels).not_to be_nil + expect(resp.channels).not_to be_empty + expect(resp.channels.first.to_h.dig('channel', 'id')).to eq(channel_id) + + end + + end + + describe 'UpdateChannel' do + + it 'updates with custom data and message, verifies custom field' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + resp = update_channel('messaging', channel_id, + data: { custom: { color: 'blue' } }, + message: { text: 'Channel updated!', user_id: @creator_id }) + expect(resp.channel).not_to be_nil + ch = resp.channel.to_h + custom = ch['custom'] || {} + expect(custom['color']).to eq('blue') + + end + + end + + describe 'PartialUpdateChannel' do + + it 'sets fields then unsets one' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Set fields + resp = update_channel_partial('messaging', channel_id, + set: { 'color' => 'red', 'description' => 'A test channel' }) + expect(resp.channel).not_to be_nil + ch = resp.channel.to_h + custom = ch['custom'] || {} + expect(custom['color']).to eq('red') + + # Unset fields + resp_2 = update_channel_partial('messaging', channel_id, unset: ['color']) + expect(resp_2.channel).not_to be_nil + ch_2 = resp_2.channel.to_h + custom_2 = ch_2['custom'] || {} + expect(custom_2).not_to have_key('color') + + end + + end + + describe 'DeleteChannel' do + + it 'soft deletes channel and verifies response' do + + channel_id = "test-del-#{SecureRandom.hex(6)}" + get_or_create_channel('messaging', channel_id, + data: { created_by_id: @creator_id }) + @created_channel_cids << "messaging:#{channel_id}" + + resp = delete_channel('messaging', channel_id) + expect(resp.channel).not_to be_nil + + end + + end + + describe 'HardDeleteChannels' do + + it 'hard deletes 2 channels via batch and polls task' do + + _type_1, channel_id_1, _resp_1 = create_test_channel(@creator_id) + _type_2, channel_id_2, _resp_2 = create_test_channel(@creator_id) + + cid_1 = "messaging:#{channel_id_1}" + cid_2 = "messaging:#{channel_id_2}" + + # Remove from tracked list since batch delete will handle it + @created_channel_cids.delete(cid_1) + @created_channel_cids.delete(cid_2) + + resp = delete_channels_batch(cids: [cid_1, cid_2], hard_delete: true) + expect(resp.task_id).not_to be_nil + + result = wait_for_task(resp.task_id) + expect(result.status).to eq('completed') + + end + + end + + describe 'AddRemoveMembers' do + + it 'adds 2 members, verifies count; removes 1, verifies removed' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Add members + update_channel('messaging', channel_id, + add_members: [{ user_id: @member_id_2 }, { user_id: @member_id_3 }]) + + # Verify members added + resp = get_or_create_channel('messaging', channel_id) + expect(resp.members.length).to be >= 4 + + # Remove a member + update_channel('messaging', channel_id, remove_members: [@member_id_3]) + + # Verify member removed + resp_2 = get_or_create_channel('messaging', channel_id) + member_ids = resp_2.members.map { |m| m.to_h['user_id'] || m.to_h.dig('user', 'id') } + expect(member_ids).not_to include(@member_id_3) + + end + + end + + describe 'QueryMembers' do + + it 'creates channel with 3 members and queries members' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1, @member_id_2] + ) + + resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: {}, + ) + expect(resp.members).not_to be_nil + expect(resp.members.length).to be >= 3 + + end + + end + + describe 'InviteAcceptReject' do + + it 'creates channel with invites, accepts one, rejects one' do + + channel_id = "test-inv-#{SecureRandom.hex(6)}" + + get_or_create_channel('messaging', channel_id, + data: { + created_by_id: @creator_id, + members: [{ user_id: @creator_id }], + invites: [{ user_id: @member_id_1 }, { user_id: @member_id_2 }], + }) + @created_channel_cids << "messaging:#{channel_id}" + + # Accept invite + update_channel('messaging', channel_id, + accept_invite: true, + user_id: @member_id_1) + + # Reject invite + update_channel('messaging', channel_id, + reject_invite: true, + user_id: @member_id_2) + + end + + end + + describe 'HideShowChannel' do + + it 'hides channel for user, then shows' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Hide + hide_channel('messaging', channel_id, user_id: @member_id_1) + + # Show + show_channel('messaging', channel_id, user_id: @member_id_1) + + end + + end + + describe 'TruncateChannel' do + + it 'sends 3 messages, truncates, verifies empty' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'Message 1') + send_test_message('messaging', channel_id, @creator_id, 'Message 2') + send_test_message('messaging', channel_id, @creator_id, 'Message 3') + + truncate_channel('messaging', channel_id) + + resp = get_or_create_channel('messaging', channel_id) + messages = resp.messages || [] + expect(messages).to be_empty + + end + + end + + describe 'FreezeUnfreezeChannel' do + + it 'sets frozen=true, verifies; sets frozen=false, verifies' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Freeze + resp = update_channel_partial('messaging', channel_id, set: { 'frozen' => true }) + expect(resp.channel.to_h['frozen']).to eq(true) + + # Unfreeze + resp_2 = update_channel_partial('messaging', channel_id, set: { 'frozen' => false }) + expect(resp_2.channel.to_h['frozen']).to eq(false) + + end + + end + + describe 'MarkReadUnread' do + + it 'sends message, marks read, marks unread' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + msg_id = send_test_message('messaging', channel_id, @creator_id, 'Message to mark read') + + # Mark read + mark_read('messaging', channel_id, user_id: @member_id_1) + + # Mark unread from this message + mark_unread('messaging', channel_id, user_id: @member_id_1, message_id: msg_id) + + end + + end + + describe 'MuteUnmuteChannel' do + + it 'mutes channel, verifies via query with muted=true; unmutes' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + cid = "messaging:#{channel_id}" + + # Mute + mute_resp = mute_channel(channel_cids: [cid], user_id: @member_id_1) + expect(mute_resp).not_to be_nil + expect(mute_resp.channel_mute).not_to be_nil + expect(mute_resp.channel_mute.to_h.dig('channel', 'cid')).to eq(cid) + + # Verify via QueryChannels with muted=true + q_resp = query_channels( + filter_conditions: { 'muted' => true, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unmute + unmute_channel(channel_cids: [cid], user_id: @member_id_1) + + # Verify unmuted + q_resp_2 = query_channels( + filter_conditions: { 'muted' => false, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp_2.channels.length).to eq(1) + + end + + end + + describe 'MemberPartialUpdate' do + + it 'sets custom fields on member; unsets one' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Set custom fields + resp = update_member_partial('messaging', channel_id, + user_id: @member_id_1, + set: { 'role_label' => 'moderator', 'score' => 42 }) + expect(resp.channel_member).not_to be_nil + member_h = resp.channel_member.to_h + custom = member_h['custom'] || {} + expect(custom['role_label']).to eq('moderator') + + # Unset a custom field + resp_2 = update_member_partial('messaging', channel_id, + user_id: @member_id_1, + unset: ['score']) + expect(resp_2.channel_member).not_to be_nil + member_h_2 = resp_2.channel_member.to_h + custom_2 = member_h_2['custom'] || {} + expect(custom_2).not_to have_key('score') + + end + + end + + describe 'AssignRoles' do + + it 'assigns channel_moderator role, verifies via QueryMembers' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Assign role + update_channel('messaging', channel_id, + assign_roles: [{ user_id: @member_id_1, channel_role: 'channel_moderator' }]) + + # Verify via QueryMembers + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id_1 }, + ) + expect(q_resp.members).not_to be_empty + expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') + + end + + end + + describe 'AddDemoteModerators' do + + it 'adds moderator, verifies; demotes, verifies back to member' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Add moderator + update_channel('messaging', channel_id, add_moderators: [@member_id_1]) + + # Verify role + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id_1 }, + ) + expect(q_resp.members).not_to be_empty + expect(q_resp.members.first.to_h['channel_role']).to eq('channel_moderator') + + # Demote + update_channel('messaging', channel_id, demote_moderators: [@member_id_1]) + + # Verify back to member + q_resp_2 = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => @member_id_1 }, + ) + expect(q_resp_2.members).not_to be_empty + expect(q_resp_2.members.first.to_h['channel_role']).to eq('channel_member') + + end + + end + + describe 'MarkUnreadWithThread' do + + it 'creates thread and marks unread from thread' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Send parent message + parent_id = send_test_message('messaging', channel_id, @creator_id, 'Parent for mark unread thread') + + # Send reply to create a thread + send_message('messaging', channel_id, + message: { text: 'Reply in thread', user_id: @creator_id, parent_id: parent_id }) + + # Mark unread from thread + mark_unread('messaging', channel_id, + user_id: @member_id_1, + thread_id: parent_id) + + end + + end + + describe 'TruncateWithOptions' do + + it 'truncates with message, skip_push, hard_delete' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'Truncate msg 1') + send_test_message('messaging', channel_id, @creator_id, 'Truncate msg 2') + + truncate_channel('messaging', channel_id, + message: { text: 'Channel was truncated', user_id: @creator_id }, + skip_push: true, + hard_delete: true) + + end + + end + + describe 'PinUnpinChannel' do + + it 'pins channel, verifies via query; unpins, verifies' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + cid = "messaging:#{channel_id}" + + # Pin + update_member_partial('messaging', channel_id, + user_id: @member_id_1, + set: { 'pinned' => true }) + + # Verify pinned + q_resp = query_channels( + filter_conditions: { 'pinned' => true, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unpin + update_member_partial('messaging', channel_id, + user_id: @member_id_1, + set: { 'pinned' => false }) + + # Verify unpinned + q_resp_2 = query_channels( + filter_conditions: { 'pinned' => false, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp_2.channels.length).to eq(1) + + end + + end + + describe 'ArchiveUnarchiveChannel' do + + it 'archives channel, verifies via query; unarchives, verifies' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + cid = "messaging:#{channel_id}" + + # Archive + update_member_partial('messaging', channel_id, + user_id: @member_id_1, + set: { 'archived' => true }) + + # Verify archived + q_resp = query_channels( + filter_conditions: { 'archived' => true, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp.channels.length).to eq(1) + expect(q_resp.channels.first.to_h.dig('channel', 'cid')).to eq(cid) + + # Unarchive + update_member_partial('messaging', channel_id, + user_id: @member_id_1, + set: { 'archived' => false }) + + # Verify unarchived + q_resp_2 = query_channels( + filter_conditions: { 'archived' => false, 'cid' => cid }, + user_id: @member_id_1, + ) + expect(q_resp_2.channels.length).to eq(1) + + end + + end + + describe 'AddMembersWithRoles' do + + it 'adds members with specific channel roles, verifies' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + new_user_ids, _resp = create_test_users(2) + mod_user_id = new_user_ids[0] + member_user_id = new_user_ids[1] + + # Add members with specific roles + update_channel('messaging', channel_id, + add_members: [ + { user_id: mod_user_id, channel_role: 'channel_moderator' }, + { user_id: member_user_id, channel_role: 'channel_member' }, + ]) + + # Query to verify roles + q_resp = query_members_api( + type: 'messaging', + id: channel_id, + filter_conditions: { 'id' => { '$in' => new_user_ids } }, + ) + + role_map = {} + q_resp.members.each do |m| + + mh = m.to_h + uid = mh['user_id'] || mh.dig('user', 'id') + role_map[uid] = mh['channel_role'] + + end + + expect(role_map[mod_user_id]).to eq('channel_moderator') + expect(role_map[member_user_id]).to eq('channel_member') + + end + + end + + describe 'MessageCount' do + + it 'sends message, queries channel, verifies message_count >= 1' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + send_test_message('messaging', channel_id, @creator_id, 'hello world') + + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id, + ) + expect(q_resp.channels.length).to eq(1) + + channel_h = q_resp.channels.first.to_h['channel'] || {} + msg_count = channel_h['message_count'] + # message_count may be nil if disabled on channel type + expect(msg_count).to be_nil.or be >= 1 + + end + + end + + describe 'SendChannelEvent' do + + it 'sends typing.start event' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + send_event('messaging', channel_id, + event: { type: 'typing.start', user_id: @creator_id }) + + end + + end + + describe 'FilterTags' do + + it 'adds filter tags, removes filter tag' do + + _type, channel_id, _resp = create_test_channel(@creator_id) + + # Add filter tags + update_channel('messaging', channel_id, + add_filter_tags: %w[sports news]) + + # Verify tags were added + resp = get_or_create_channel('messaging', channel_id) + expect(resp.channel).not_to be_nil + + # Remove a filter tag + update_channel('messaging', channel_id, + remove_filter_tags: ['sports']) + + end + + end + + describe 'MessageCountDisabled' do + + it 'disables count_messages via config_overrides, verifies message_count nil' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Disable count_messages + update_channel_partial('messaging', channel_id, + set: { + 'config_overrides' => { 'count_messages' => false }, + }) + + send_test_message('messaging', channel_id, @creator_id, 'hello world disabled count') + + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id, + ) + expect(q_resp.channels.length).to eq(1) + + channel_h = q_resp.channels.first.to_h['channel'] || {} + expect(channel_h['message_count']).to be_nil + + end + + end + + describe 'MarkUnreadWithTimestamp' do + + it 'sends message, gets timestamp, marks unread from timestamp' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id, @member_id_1] + ) + + # Send message to get a valid timestamp + resp = send_message('messaging', channel_id, + message: { text: 'test message for timestamp unread', user_id: @creator_id }) + created_at = resp.message.to_h['created_at'] + expect(created_at).not_to be_nil + + # API may return created_at as nanosecond epoch integer; convert to RFC 3339 string + ts = if created_at.is_a?(Numeric) + Time.at(0, created_at, :nanosecond).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + else + created_at.to_s + end + + # Mark unread from timestamp + mark_unread('messaging', channel_id, + user_id: @member_id_1, + message_timestamp: ts) + + end + + end + + describe 'HideForCreator' do + + it 'creates channel with hide_for_creator=true, verifies hidden' do + + channel_id = "test-hide-#{SecureRandom.hex(6)}" + + get_or_create_channel('messaging', channel_id, + hide_for_creator: true, + data: { + created_by_id: @creator_id, + members: [ + { user_id: @creator_id }, + { user_id: @member_id_1 }, + ], + }) + @created_channel_cids << "messaging:#{channel_id}" + + # Channel should be hidden for creator + q_resp = query_channels( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + user_id: @creator_id, + ) + expect(q_resp.channels).to be_empty + + end + + end + + describe 'UploadAndDeleteFile' do + + it 'uploads a text file, verifies URL, deletes file' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id] + ) + + # Create a temp file + tmpfile = Tempfile.new(['chat-test-', '.txt']) + tmpfile.write('hello world test file content') + tmpfile.close + + begin + upload_resp = upload_channel_file( + 'messaging', channel_id, + GetStream::Generated::Models::FileUploadRequest.new( + file: tmpfile.path, + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id), + ) + ) + expect(upload_resp.file).not_to be_nil + file_url = upload_resp.file + expect(file_url).to include('http') + + # Delete file + delete_channel_file('messaging', channel_id, file_url) + ensure + tmpfile.unlink + end + + end + + end + + describe 'UploadAndDeleteImage' do + + it 'uploads an image file, verifies URL, deletes image' do + + _type, channel_id, _resp = create_test_channel_with_members( + @creator_id, [@creator_id] + ) + + # Use existing test image + image_path = File.join(__dir__, 'upload-test.png') + skip('upload-test.png not found') unless File.exist?(image_path) + + upload_resp = upload_channel_image( + 'messaging', channel_id, + GetStream::Generated::Models::ImageUploadRequest.new( + file: image_path, + user: GetStream::Generated::Models::OnlyUserID.new(id: @creator_id), + ) + ) + expect(upload_resp.file).not_to be_nil + image_url = upload_resp.file + expect(image_url).to include('http') + + # Delete image + delete_channel_image('messaging', channel_id, image_url) + + end + + end + +end diff --git a/spec/integration/chat_message_integration_spec.rb b/spec/integration/chat_message_integration_spec.rb new file mode 100644 index 0000000..0980d22 --- /dev/null +++ b/spec/integration/chat_message_integration_spec.rb @@ -0,0 +1,717 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Message Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + # Create shared test users for all subtests + @shared_user_ids, _resp = create_test_users(3) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + + end + + after(:all) do + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Message API wrappers + # --------------------------------------------------------------------------- + + def get_message(message_id) + @client.make_request(:get, "/api/v2/chat/messages/#{message_id}") + end + + def get_many_messages(type, id, message_ids) + @client.make_request( + :get, + "/api/v2/chat/channels/#{type}/#{id}/messages", + query_params: { 'ids' => message_ids.join(',') }, + ) + end + + def update_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}", body: body) + end + + def update_message_partial(message_id, body) + @client.make_request(:put, "/api/v2/chat/messages/#{message_id}", body: body) + end + + def delete_message(message_id, query_params = {}) + @client.make_request(:delete, "/api/v2/chat/messages/#{message_id}", query_params: query_params) + end + + def send_msg(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/message", body: body) + end + + def translate_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/translate", body: body) + end + + def get_replies(parent_id, **query_params) + @client.make_request(:get, "/api/v2/chat/messages/#{parent_id}/replies", query_params: query_params) + end + + def search_messages(body) + @client.make_request(:get, '/api/v2/chat/search', query_params: { 'payload' => JSON.generate(body) }) + end + + def commit_message(message_id) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/commit") + end + + def query_message_history(body) + @client.make_request(:post, '/api/v2/chat/messages/history', body: body) + end + + def hide_channel(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/hide", body: body) + end + + def undelete_message(message_id, body) + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/undelete", body: body) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'SendAndGetMessage' do + + it 'sends message, gets by ID, verifies text' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + msg_text = "Hello from integration test #{SecureRandom.hex(8)}" + send_resp = send_msg('messaging', channel_id, + message: { text: msg_text, user_id: @user_1 }) + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(msg_id).not_to be_nil + expect(send_resp.message.to_h['text']).to eq(msg_text) + + # Get message by ID + get_resp = get_message(msg_id) + expect(get_resp.message).not_to be_nil + expect(get_resp.message.to_h['id']).to eq(msg_id) + expect(get_resp.message.to_h['text']).to eq(msg_text) + + end + + end + + describe 'GetManyMessages' do + + it 'sends 3 messages, gets all 3 by IDs' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + id_1 = send_test_message('messaging', channel_id, @user_1, 'Msg 1') + id_2 = send_test_message('messaging', channel_id, @user_1, 'Msg 2') + id_3 = send_test_message('messaging', channel_id, @user_1, 'Msg 3') + + resp = get_many_messages('messaging', channel_id, [id_1, id_2, id_3]) + expect(resp.messages).not_to be_nil + expect(resp.messages.length).to eq(3) + + end + + end + + describe 'UpdateMessage' do + + it 'sends message, updates text, verifies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Original text') + + updated_text = "Updated text #{SecureRandom.hex(8)}" + resp = update_message(msg_id, message: { text: updated_text, user_id: @user_1 }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['text']).to eq(updated_text) + + end + + end + + describe 'PartialUpdateMessage' do + + it 'sets custom fields; unsets one' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Partial update test') + + # Set custom fields + resp = update_message_partial(msg_id, + set: { 'priority' => 'high', 'status' => 'reviewed' }, + user_id: @user_1) + expect(resp.message).not_to be_nil + + # Unset custom field + resp_2 = update_message_partial(msg_id, + unset: ['status'], + user_id: @user_1) + expect(resp_2.message).not_to be_nil + + end + + end + + describe 'DeleteMessage' do + + it 'soft deletes, verifies type=deleted' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to delete') + + resp = delete_message(msg_id) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('deleted') + + end + + end + + describe 'HardDeleteMessage' do + + it 'hard deletes, verifies type=deleted' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to hard delete') + + resp = delete_message(msg_id, { 'hard' => 'true' }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('deleted') + + end + + end + + describe 'PinUnpinMessage' do + + it 'sends pinned message; unpins via partial update' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + # Send a pinned message + send_resp = send_msg('messaging', channel_id, + message: { text: 'Pinned message', user_id: @user_1, pinned: true }) + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(send_resp.message.to_h['pinned']).to eq(true) + + # Unpin via partial update + resp = update_message_partial(msg_id, + set: { 'pinned' => false }, + user_id: @user_1) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['pinned']).to eq(false) + + end + + end + + describe 'TranslateMessage' do + + it 'translates to Spanish, verifies i18n field' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Hello, how are you?') + + resp = translate_message(msg_id, language: 'es') + expect(resp.message).not_to be_nil + i18n = resp.message.to_h['i18n'] + expect(i18n).not_to be_nil + + end + + end + + describe 'ThreadReply' do + + it 'sends parent, sends reply with parent_id, gets replies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + + # Send parent message + parent_id = send_test_message('messaging', channel_id, @user_1, 'Parent message for thread') + + # Send reply + reply_resp = send_msg('messaging', channel_id, + message: { text: 'Reply to parent', user_id: @user_2, parent_id: parent_id }) + expect(reply_resp.message).not_to be_nil + expect(reply_resp.message.id).not_to be_nil + + # Get replies + replies_resp = get_replies(parent_id) + expect(replies_resp.messages).not_to be_nil + expect(replies_resp.messages.length).to be >= 1 + + end + + end + + describe 'SearchMessages' do + + it 'sends message with unique term, waits, searches, verifies found' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + search_term = "uniquesearch#{SecureRandom.hex(8)}" + send_test_message('messaging', channel_id, @user_1, "This message contains #{search_term} for testing") + + # Wait for indexing + sleep(2) + + resp = search_messages( + query: search_term, + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + ) + expect(resp.results).not_to be_nil + expect(resp.results).not_to be_empty + + end + + end + + describe 'SilentMessage' do + + it 'sends with silent=true, verifies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + resp = send_msg('messaging', channel_id, + message: { text: 'This is a silent message', user_id: @user_1, silent: true }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['silent']).to eq(true) + + end + + end + + describe 'PendingMessage' do + + it 'sends pending, commits, verifies (skip if not enabled)' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + begin + send_resp = send_msg('messaging', channel_id, + message: { text: 'Pending message text', user_id: @user_1 }, + pending: true, + skip_push: true) + rescue GetStreamRuby::APIError => e + if e.message.include?('pending messages not enabled') || e.message.include?('feature flag') + skip('Pending messages feature not enabled for this app') + end + raise + end + + expect(send_resp.message).not_to be_nil + msg_id = send_resp.message.id + expect(msg_id).not_to be_nil + + # Commit the pending message + commit_resp = commit_message(msg_id) + expect(commit_resp.message).not_to be_nil + expect(commit_resp.message.to_h['id']).to eq(msg_id) + + end + + end + + describe 'QueryMessageHistory' do + + it 'sends, updates twice, queries history, verifies entries (skip if not enabled)' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + + # Send initial message + send_resp = send_msg('messaging', channel_id, + message: { text: 'initial text', user_id: @user_1, + custom: { 'custom_field' => 'custom value' } }) + msg_id = send_resp.message.id + + # Update by user1 + update_message(msg_id, message: { text: 'updated text', user_id: @user_1, + custom: { 'custom_field' => 'updated custom value' } }) + + # Update by user2 + update_message(msg_id, message: { text: 'updated text 2', user_id: @user_2 }) + + # Query message history + begin + hist_resp = query_message_history( + filter: { 'message_id' => msg_id }, + sort: [], + ) + rescue GetStreamRuby::APIError => e + if e.message.include?('feature flag') || e.message.include?('not enabled') + skip('QueryMessageHistory feature not enabled for this app') + end + raise + end + + expect(hist_resp.message_history).not_to be_nil + expect(hist_resp.message_history.length).to be >= 2 + + # Verify history entries reference the correct message + hist_resp.message_history.each do |entry| + + h = entry.to_h + expect(h['message_id']).to eq(msg_id) + + end + + # Verify text values (descending by default: most recent first) + expect(hist_resp.message_history.first.to_h['text']).to eq('updated text 2') + expect(hist_resp.message_history.last.to_h['text']).to eq('initial text') + + end + + end + + describe 'QueryMessageHistorySort' do + + it 'queries history with ascending sort' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'sort initial', user_id: @user_1 }) + msg_id = send_resp.message.id + + update_message(msg_id, message: { text: 'sort updated 1', user_id: @user_1 }) + update_message(msg_id, message: { text: 'sort updated 2', user_id: @user_1 }) + + begin + hist_resp = query_message_history( + filter: { 'message_id' => msg_id }, + sort: [{ 'field' => 'message_updated_at', 'direction' => 1 }], + ) + rescue GetStreamRuby::APIError => e + if e.message.include?('feature flag') || e.message.include?('not enabled') + skip('QueryMessageHistory feature not enabled for this app') + end + raise + end + + expect(hist_resp.message_history).not_to be_nil + expect(hist_resp.message_history.length).to be >= 2 + + # Ascending: oldest first + expect(hist_resp.message_history[0].to_h['text']).to eq('sort initial') + + end + + end + + describe 'SkipEnrichUrl' do + + it 'sends with URL and skip_enrich_url=true, verifies no attachments' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'Check out https://getstream.io for more info', user_id: @user_1 }, + skip_enrich_url: true) + expect(send_resp.message).not_to be_nil + attachments = send_resp.message.to_h['attachments'] || [] + expect(attachments).to be_empty + + # Verify via GetMessage that attachments remain empty + sleep(1) + get_resp = get_message(send_resp.message.id) + attachments_2 = get_resp.message.to_h['attachments'] || [] + expect(attachments_2).to be_empty + + end + + end + + describe 'KeepChannelHidden' do + + it 'hides channel, sends with keep_channel_hidden=true, verifies still hidden' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + cid = "messaging:#{channel_id}" + + # Hide the channel + hide_channel('messaging', channel_id, user_id: @user_1) + + # Send a message with keep_channel_hidden=true + send_msg('messaging', channel_id, + message: { text: 'Hidden message', user_id: @user_1 }, + keep_channel_hidden: true) + + # Query channels — the channel should still be hidden + q_resp = query_channels( + filter_conditions: { 'cid' => cid }, + user_id: @user_1, + ) + expect(q_resp.channels).to be_empty + + end + + end + + describe 'UndeleteMessage' do + + it 'soft deletes, undeletes, verifies restored' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'Message to undelete') + + # Soft delete + delete_message(msg_id) + + # Verify deleted + get_resp = get_message(msg_id) + expect(get_resp.message.to_h['type']).to eq('deleted') + + # Undelete + begin + undel_resp = undelete_message(msg_id, undeleted_by: @user_1) + rescue GetStreamRuby::APIError => e + if e.message.include?('undeleted_by') || e.message.include?('required field') + skip('UndeleteMessage requires undeleted_by field not yet in generated request struct') + end + raise + end + expect(undel_resp.message).not_to be_nil + expect(undel_resp.message.to_h['type']).not_to eq('deleted') + expect(undel_resp.message.to_h['text']).to eq('Message to undelete') + + end + + end + + describe 'RestrictedVisibility' do + + it 'sends with restricted_visibility list (skip if not enabled)' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + + begin + send_resp = send_msg('messaging', channel_id, + message: { text: 'Secret message', user_id: @user_1, + restricted_visibility: [@user_1] }) + rescue GetStreamRuby::APIError => e + if e.message.include?('private messaging is not allowed') || e.message.include?('not enabled') + skip('RestrictedVisibility (private messaging) is not enabled for this app') + end + raise + end + + expect(send_resp.message.to_h['restricted_visibility']).to eq([@user_1]) + + end + + end + + describe 'DeleteMessageForMe' do + + it 'deletes message with delete_for_me=true' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, 'test message to delete for me') + + delete_message(msg_id, { 'delete_for_me' => 'true', 'deleted_by' => @user_1 }) + + end + + end + + describe 'PinExpiration' do + + it 'pins with 3s expiry, waits 4s, verifies expired' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_2, 'Message to pin with expiry') + + # Pin with 3 second expiration + expiry = (Time.now.utc + 3).strftime('%Y-%m-%dT%H:%M:%S.%6NZ') + pin_resp = update_message_partial(msg_id, + set: { 'pinned' => true, 'pin_expires' => expiry }, + user_id: @user_1) + expect(pin_resp.message).not_to be_nil + expect(pin_resp.message.to_h['pinned']).to eq(true) + + # Wait for pin to expire + sleep(4) + + # Verify pin expired + get_resp = get_message(msg_id) + expect(get_resp.message.to_h['pinned']).to eq(false) + + end + + end + + describe 'SystemMessage' do + + it 'sends with type=system, verifies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + resp = send_msg('messaging', channel_id, + message: { text: 'User joined the channel', user_id: @user_1, type: 'system' }) + expect(resp.message).not_to be_nil + expect(resp.message.to_h['type']).to eq('system') + + end + + end + + describe 'PendingFalse' do + + it 'sends with pending=false, verifies immediately available' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + send_resp = send_msg('messaging', channel_id, + message: { text: 'Non-pending message', user_id: @user_1 }, + pending: false) + expect(send_resp.message).not_to be_nil + + # Get the message to verify it's immediately available + get_resp = get_message(send_resp.message.id) + expect(get_resp.message.to_h['text']).to eq('Non-pending message') + + end + + end + + describe 'SearchWithMessageFilters' do + + it 'searches using message_filter_conditions' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + + search_term = "filterable#{SecureRandom.hex(8)}" + send_test_message('messaging', channel_id, @user_1, "This has #{search_term} text") + send_test_message('messaging', channel_id, @user_1, "This also has #{search_term} text") + + # Wait for indexing + sleep(2) + + resp = search_messages( + filter_conditions: { 'cid' => "messaging:#{channel_id}" }, + message_filter_conditions: { 'text' => { '$q' => search_term } }, + ) + expect(resp.results).not_to be_nil + expect(resp.results.length).to be >= 2 + + end + + end + + describe 'SearchQueryAndMessageFiltersError' do + + it 'verifies error when using both query and message_filter_conditions' do + + params = { + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + message_filter_conditions: { 'text' => { '$q' => 'test' } }, + } + expect { search_messages(**params) }.to raise_error(GetStreamRuby::APIError) + + end + + end + + describe 'SearchOffsetAndSortError' do + + it 'verifies error when using offset with sort' do + + # The API may or may not reject offset+sort. Verify either an error or a valid response. + + resp = search_messages( + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + offset: 1, + sort: [{ 'field' => 'created_at', 'direction' => -1 }], + ) + # If no error, the API accepts the combination — verify a valid response + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError + # Expected error — test passes + + end + + end + + describe 'SearchOffsetAndNextError' do + + it 'verifies error when using offset with next' do + + params = { + filter_conditions: { 'members' => { '$in' => [@user_1] } }, + query: 'test', + offset: 1, + next: SecureRandom.hex(5), + } + expect { search_messages(**params) }.to raise_error(GetStreamRuby::APIError) + + end + + end + + describe 'ChannelRoleInMember' do + + it 'creates channel with roles, sends messages, verifies member.channel_role in response' do + + role_user_ids, _resp = create_test_users(2) + member_user_id = role_user_ids[0] + mod_user_id = role_user_ids[1] + + channel_id = "test-ch-#{SecureRandom.hex(6)}" + @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: { + data: { + created_by_id: member_user_id, + members: [ + { user_id: member_user_id, channel_role: 'channel_member' }, + { user_id: mod_user_id, channel_role: 'channel_moderator' }, + ], + }, + }, + ) + @created_channel_cids << "messaging:#{channel_id}" + + # Send message from channel_member + resp_member = send_msg('messaging', channel_id, + message: { text: 'message from channel_member', user_id: member_user_id }) + expect(resp_member.message).not_to be_nil + member_data = resp_member.message.to_h['member'] || {} + expect(member_data['channel_role']).to eq('channel_member') + + # Send message from channel_moderator + resp_mod = send_msg('messaging', channel_id, + message: { text: 'message from channel_moderator', user_id: mod_user_id }) + expect(resp_mod.message).not_to be_nil + mod_data = resp_mod.message.to_h['member'] || {} + expect(mod_data['channel_role']).to eq('channel_moderator') + + end + + end + +end diff --git a/spec/integration/chat_misc_integration_spec.rb b/spec/integration/chat_misc_integration_spec.rb new file mode 100644 index 0000000..99eea9d --- /dev/null +++ b/spec/integration/chat_misc_integration_spec.rb @@ -0,0 +1,775 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Misc Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @shared_user_ids, _resp = create_test_users(4) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] + @created_blocklist_names = [] + @created_command_names = [] + @created_channel_type_names = [] + @created_role_names = [] + + end + + after(:all) do + + # Clean up blocklists + @created_blocklist_names&.each do |name| + + @client.common.delete_block_list(name) + rescue StandardError => e + puts "Warning: Failed to delete blocklist #{name}: #{e.message}" + + end + + # Clean up commands + @created_command_names&.each do |name| + + @client.make_request(:delete, "/api/v2/chat/commands/#{name}") + rescue StandardError => e + puts "Warning: Failed to delete command #{name}: #{e.message}" + + end + + # Clean up channel types (with retry due to eventual consistency) + @created_channel_type_names&.each do |name| + + 3.times do |i| + + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{name}") + break + rescue StandardError => e + puts "Warning: Failed to delete channel type #{name} (attempt #{i + 1}): #{e.message}" + sleep(1) + + end + + end + + # Clean up roles + @created_role_names&.each do |name| + + 3.times do |i| + + @client.common.delete_role(name) + break + rescue StandardError => e + puts "Warning: Failed to delete role #{name} (attempt #{i + 1}): #{e.message}" + sleep(1) + + end + + end + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Devices + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteDevice' do + + it 'creates a firebase device, lists it, deletes it, and verifies gone' do + + device_id = "integration-test-device-#{random_string(12)}" + + # Create device + @client.common.create_device( + GetStream::Generated::Models::CreateDeviceRequest.new( + id: device_id, + push_provider: 'firebase', + user_id: @user_1, + ), + ) + + # List devices + list_resp = @client.common.list_devices(@user_1) + devices = list_resp.devices || [] + found = devices.any? do |d| + + h = d.is_a?(Hash) ? d : d.to_h + + h['id'] == device_id + + end + expect(found).to be(true), 'Created device should appear in list' + + # Delete device + @client.common.delete_device(device_id, @user_1) + + # Verify deleted + list_resp_2 = @client.common.list_devices(@user_1) + devices_2 = list_resp_2.devices || [] + still_found = devices_2.any? do |d| + + h = d.is_a?(Hash) ? d : d.to_h + + h['id'] == device_id + + end + expect(still_found).to be(false), 'Device should be deleted' + rescue GetStreamRuby::APIError => e + if e.message.include?('push provider') || e.message.include?('no push') + skip('Push providers not configured for this app') + end + raise + + end + + end + + # --------------------------------------------------------------------------- + # Blocklists + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteBlocklist' do + + it 'creates a custom blocklist, lists it, verifies found, and deletes it' do + + blocklist_name = "test-blocklist-#{random_string(8)}" + + # Create blocklist + @client.common.create_block_list( + GetStream::Generated::Models::CreateBlockListRequest.new( + name: blocklist_name, + words: %w[badword1 badword2 badword3], + ), + ) + @created_blocklist_names << blocklist_name + + # Get blocklist and verify + get_resp = @client.common.get_block_list(blocklist_name) + expect(get_resp.blocklist).not_to be_nil + bl_h = get_resp.blocklist.to_h + expect(bl_h['name']).to eq(blocklist_name) + expect(bl_h['words'].length).to eq(3) + + # Update blocklist + @client.common.update_block_list( + blocklist_name, + GetStream::Generated::Models::UpdateBlockListRequest.new( + words: %w[badword1 badword2 badword3 badword4], + ), + ) + + # Verify update + get_resp_2 = @client.common.get_block_list(blocklist_name) + bl_h_2 = get_resp_2.blocklist.to_h + expect(bl_h_2['words'].length).to eq(4) + + # List blocklists and verify found + list_resp = @client.common.list_block_lists + blocklists = list_resp.blocklists || [] + found = blocklists.any? do |bl| + + h = bl.is_a?(Hash) ? bl : bl.to_h + h['name'] == blocklist_name + + end + expect(found).to be(true), 'Created blocklist should appear in list' + + # Delete a separate blocklist to test deletion + del_name = "test-del-bl-#{random_string(8)}" + @client.common.create_block_list( + GetStream::Generated::Models::CreateBlockListRequest.new( + name: del_name, + words: %w[word1], + ), + ) + @created_blocklist_names << del_name + @client.common.delete_block_list(del_name) + @created_blocklist_names.delete(del_name) + + end + + end + + # --------------------------------------------------------------------------- + # Commands + # --------------------------------------------------------------------------- + + describe 'CreateListDeleteCommand' do + + it 'creates a custom command, lists it, verifies found, and deletes it' do + + cmd_name = "testcmd#{random_string(6)}" + + # Create command + resp = @client.make_request(:post, '/api/v2/chat/commands', body: { + name: cmd_name, + description: 'A test command', + }) + expect(resp).not_to be_nil + @created_command_names << cmd_name + + # Get command + get_resp = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") + expect(get_resp.name).to eq(cmd_name) + expect(get_resp.description).to eq('A test command') + + # Update command + @client.make_request(:put, "/api/v2/chat/commands/#{cmd_name}", body: { + description: 'Updated test command', + }) + + # Verify update + get_resp_2 = @client.make_request(:get, "/api/v2/chat/commands/#{cmd_name}") + expect(get_resp_2.description).to eq('Updated test command') + + # List commands + list_resp = @client.make_request(:get, '/api/v2/chat/commands') + commands = list_resp.commands || [] + found = commands.any? do |c| + + h = c.is_a?(Hash) ? c : c.to_h + h['name'] == cmd_name + + end + expect(found).to be(true), 'Created command should appear in list' + + # Delete a separate command + del_name = "testdelcmd#{random_string(6)}" + @client.make_request(:post, '/api/v2/chat/commands', body: { + name: del_name, + description: 'Command to delete', + }) + del_resp = @client.make_request(:delete, "/api/v2/chat/commands/#{del_name}") + expect(del_resp).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # Channel Types + # --------------------------------------------------------------------------- + + describe 'CreateUpdateDeleteChannelType' do + + it 'creates a channel type, updates settings, verifies, and deletes' do + + type_name = "testtype#{random_string(6)}" + + # Create channel type with a lower max_message_length so the update below + # can demonstrate the value actually changes. The test app plan caps at + # 4000, so stay within that ceiling to avoid silent truncation. + create_resp = @client.make_request(:post, '/api/v2/chat/channeltypes', body: { + name: type_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 3000, + }) + expect(create_resp.name).to eq(type_name) + @created_channel_type_names << type_name + + # Wait for eventual consistency + sleep(2) + + # Get channel type + get_resp = @client.make_request(:get, "/api/v2/chat/channeltypes/#{type_name}") + expect(get_resp.name).to eq(type_name) + + # Update channel type — raise to 4000 (plan maximum) to verify the + # update is applied. Assert on the update response directly: it is read + # from the writing server's local cache (always fresh) so we avoid the + # eventual consistency window that makes a re-fetch flaky. + update_resp = @client.make_request(:put, "/api/v2/chat/channeltypes/#{type_name}", body: { + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 4000, + typing_events: false, + }) + expect(update_resp.max_message_length).to eq(4000) + + # Delete a separate channel type + del_name = "testdeltype#{random_string(6)}" + @client.make_request(:post, '/api/v2/chat/channeltypes', body: { + name: del_name, + automod: 'disabled', + automod_behavior: 'flag', + max_message_length: 4000, + }) + @created_channel_type_names << del_name + + sleep(2) + + delete_err = nil + 5.times do |_i| + + @client.make_request(:delete, "/api/v2/chat/channeltypes/#{del_name}") + @created_channel_type_names.delete(del_name) + delete_err = nil + break + rescue StandardError => e + delete_err = e + sleep(2) + + end + expect(delete_err).to be_nil, "Channel type deletion should succeed: #{delete_err&.message}" + + end + + end + + describe 'ListChannelTypes' do + + it 'lists all channel types and verifies default types present' do + + resp = @client.make_request(:get, '/api/v2/chat/channeltypes') + expect(resp.channel_types).not_to be_nil + + types_h = resp.channel_types.to_h + expect(types_h.key?('messaging')).to be(true), "Default 'messaging' type should be present" + + end + + end + + # --------------------------------------------------------------------------- + # Permissions & Roles + # --------------------------------------------------------------------------- + + describe 'ListPermissions' do + + it 'lists all permissions and verifies non-empty' do + + resp = @client.common.list_permissions + expect(resp.permissions).not_to be_nil + expect(resp.permissions.length).to be > 0 + + end + + end + + describe 'CreatePermission' do + + it 'creates a custom role, lists it, and verifies custom flag' do + + role_name = "testrole#{random_string(6)}" + + # Create role + @client.common.create_role( + GetStream::Generated::Models::CreateRoleRequest.new(name: role_name), + ) + @created_role_names << role_name + + # List roles and verify + list_resp = @client.common.list_roles + roles = list_resp.roles || [] + found = roles.any? do |r| + + h = r.is_a?(Hash) ? r : r.to_h + h['name'] == role_name && h['custom'] == true + + end + expect(found).to be(true), 'Created role should appear in list as custom' + + end + + end + + describe 'GetPermission' do + + it 'gets a specific permission by ID' do + + resp = @client.common.get_permission('create-channel') + expect(resp.permission).not_to be_nil + perm_h = resp.permission.to_h + expect(perm_h['id']).to eq('create-channel') + expect(perm_h['action']).not_to be_nil + expect(perm_h['action']).not_to be_empty + + end + + end + + # --------------------------------------------------------------------------- + # Banned Users + # --------------------------------------------------------------------------- + + describe 'QueryBannedUsers' do + + it 'bans a user in channel, queries banned users, and verifies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + cid = "messaging:#{channel_id}" + + # Ban user in channel + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user_2, + banned_by_id: @user_1, + channel_cid: cid, + reason: 'test ban reason', + timeout: 60, + ), + ) + + # Query banned users + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + ban_h = bans[0].is_a?(Hash) ? bans[0] : bans[0].to_h + expect(ban_h['reason']).to eq('test ban reason') + + # Unban + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user_2, + cid, + ) + + # Verify ban is gone + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + expect(bans_2.length).to eq(0), 'Bans should be empty after unban' + + end + + end + + # --------------------------------------------------------------------------- + # Mute/Unmute User + # --------------------------------------------------------------------------- + + describe 'MuteUnmuteUser' do + + it 'mutes user, verifies via query, and unmutes' do + + # Mute user + mute_resp = @client.moderation.mute( + GetStream::Generated::Models::MuteRequest.new( + target_ids: [@user_3], + user_id: @user_1, + ), + ) + expect(mute_resp.mutes).not_to be_nil + expect(mute_resp.mutes.length).to be >= 1 + + mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h + expect(mute_h['target']).not_to be_nil + + # Verify via QueryUsers that user has mutes + q_resp = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) + expect(q_resp.users).not_to be_nil + expect(q_resp.users.length).to be >= 1 + user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h + expect(user_h['mutes']).not_to be_nil + expect(user_h['mutes'].length).to be >= 1 + + # Unmute + @client.moderation.unmute( + GetStream::Generated::Models::UnmuteRequest.new( + target_ids: [@user_3], + user_id: @user_1, + ), + ) + + end + + end + + # --------------------------------------------------------------------------- + # App Settings + # --------------------------------------------------------------------------- + + describe 'GetAppSettings' do + + it 'gets app settings and verifies response' do + + resp = @client.common.get_app + expect(resp).not_to be_nil + expect(resp.app).not_to be_nil + app_h = resp.app.to_h + expect(app_h['name']).not_to be_nil + expect(app_h['name']).not_to be_empty + + end + + end + + # --------------------------------------------------------------------------- + # Export Channels + # --------------------------------------------------------------------------- + + describe 'ExportChannels' do + + it 'exports channel messages and polls task until completed' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + send_test_message('messaging', channel_id, @user_1, "Message for export test #{SecureRandom.hex(4)}") + + cid = "messaging:#{channel_id}" + + # Export channels + export_resp = @client.make_request(:post, '/api/v2/chat/export_channels', body: { + channels: [{ cid: cid }], + }) + expect(export_resp.task_id).not_to be_nil + expect(export_resp.task_id).not_to be_empty + + # Wait for task + task_result = wait_for_task(export_resp.task_id) + expect(task_result.status).to eq('completed') + + end + + end + + # --------------------------------------------------------------------------- + # Threads + # --------------------------------------------------------------------------- + + describe 'Threads' do + + it 'creates parent + replies, queries threads, and verifies' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + channel_cid = "messaging:#{channel_id}" + + # Create thread: parent message + replies + parent_id = send_test_message('messaging', channel_id, @user_1, 'Thread parent message') + + send_message('messaging', channel_id, { + message: { + text: 'First reply in thread', + user_id: @user_2, + parent_id: parent_id, + }, + }) + + send_message('messaging', channel_id, { + message: { + text: 'Second reply in thread', + user_id: @user_1, + parent_id: parent_id, + }, + }) + + # Query threads + resp = @client.make_request(:post, '/api/v2/chat/threads', body: { + user_id: @user_1, + filter: { + 'channel_cid' => { '$eq' => channel_cid }, + }, + }) + expect(resp.threads).not_to be_nil + expect(resp.threads.length).to be >= 1 + + found = resp.threads.any? do |t| + + h = t.is_a?(Hash) ? t : t.to_h + h['parent_message_id'] == parent_id + + end + expect(found).to be(true), 'Thread should appear in query results' + + # Get thread + get_resp = @client.make_request(:get, "/api/v2/chat/threads/#{parent_id}", query_params: { + 'reply_limit' => '10', + }) + thread_h = get_resp.thread.is_a?(Hash) ? get_resp.thread : get_resp.thread.to_h + expect(thread_h['parent_message_id']).to eq(parent_id) + latest_replies = thread_h['latest_replies'] || [] + expect(latest_replies.length).to be >= 2 + + end + + end + + # --------------------------------------------------------------------------- + # Unread Counts + # --------------------------------------------------------------------------- + + describe 'GetUnreadCounts' do + + it 'sends message and gets unread counts for user' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + send_test_message('messaging', channel_id, @user_1, "Unread test #{SecureRandom.hex(4)}") + + resp = @client.make_request(:get, '/api/v2/chat/unread', query_params: { + 'user_id' => @user_2, + }) + expect(resp).not_to be_nil + expect(resp.total_unread_count).to be >= 0 + + end + + end + + describe 'GetUnreadCountsBatch' do + + it 'gets unread counts for multiple users' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + send_test_message('messaging', channel_id, @user_1, "Batch unread test #{SecureRandom.hex(4)}") + + resp = @client.make_request(:post, '/api/v2/chat/unread_batch', body: { + user_ids: [@user_1, @user_2], + }) + expect(resp).not_to be_nil + expect(resp.counts_by_user).not_to be_nil + counts_h = resp.counts_by_user.to_h + expect(counts_h.key?(@user_1)).to be(true) + expect(counts_h.key?(@user_2)).to be(true) + + end + + end + + # --------------------------------------------------------------------------- + # Reminders + # --------------------------------------------------------------------------- + + describe 'Reminders' do + + it 'creates a reminder, lists it, updates it, and deletes it' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Reminder test #{SecureRandom.hex(4)}") + + remind_at = (Time.now + (24 * 3600)).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + + # Create reminder + create_resp = @client.make_request(:post, "/api/v2/chat/messages/#{msg_id}/reminders", body: { + user_id: @user_1, + remind_at: remind_at, + }) + expect(create_resp).not_to be_nil + + # Query reminders + query_resp = @client.make_request(:post, '/api/v2/chat/reminders/query', body: { + user_id: @user_1, + filter: { 'message_id' => msg_id }, + sort: [], + }) + reminders = query_resp.reminders || [] + expect(reminders.length).to be >= 1 + + # Update reminder + new_remind_at = (Time.now + (48 * 3600)).utc.strftime('%Y-%m-%dT%H:%M:%S.%9NZ') + update_resp = @client.make_request(:patch, "/api/v2/chat/messages/#{msg_id}/reminders", body: { + user_id: @user_1, + remind_at: new_remind_at, + }) + expect(update_resp).not_to be_nil + + # Delete reminder + @client.make_request(:delete, "/api/v2/chat/messages/#{msg_id}/reminders", query_params: { + 'user_id' => @user_1, + }) + rescue GetStreamRuby::APIError => e + skip('Reminders not enabled for this app') if e.message.include?('not enabled') || e.message.include?('reminder') + raise + + end + + end + + # --------------------------------------------------------------------------- + # Send User Custom Event + # --------------------------------------------------------------------------- + + describe 'SendUserCustomEvent' do + + it 'sends a custom event to a user' do + + resp = @client.make_request(:post, "/api/v2/chat/users/#{@user_1}/event", body: { + event: { + type: 'friendship_request', + message: "Let's be friends!", + }, + }) + expect(resp).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # Query Team Usage Stats + # --------------------------------------------------------------------------- + + describe 'QueryTeamUsageStats' do + + it 'queries team usage stats' do + + resp = @client.make_request(:post, '/api/v2/chat/stats/team_usage', body: {}) + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError => e + if e.message.include?('Token signature') || + e.message.include?('not available') || + e.message.include?('not found') || + e.message.include?('Not Found') + skip('QueryTeamUsageStats not available on this app') + end + raise + + end + + end + + # --------------------------------------------------------------------------- + # Channel Batch Update + # --------------------------------------------------------------------------- + + describe 'ChannelBatchUpdate' do + + it 'batch updates multiple channels at once' do + + _type_1, ch_id_1, _resp_1 = create_test_channel(@user_1) + _type_2, ch_id_2, _resp_2 = create_test_channel(@user_1) + + # Batch update: set a custom field on both channels + cids = ["messaging:#{ch_id_1}", "messaging:#{ch_id_2}"] + + resp = @client.make_request(:post, '/api/v2/chat/channels/batch_update', body: { + set: { 'color' => 'blue' }, + filter: { + 'cid' => { '$in' => cids }, + }, + }) + expect(resp).not_to be_nil + rescue GetStreamRuby::APIError => e + if e.message.include?('not available') || + e.message.include?('Not Found') || + e.message.include?('unknown') || + e.message.include?('not found') + skip('Channel batch update not available') + end + raise + + end + + end + +end diff --git a/spec/integration/chat_moderation_integration_spec.rb b/spec/integration/chat_moderation_integration_spec.rb new file mode 100644 index 0000000..7f290f0 --- /dev/null +++ b/spec/integration/chat_moderation_integration_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Moderation Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @shared_user_ids, _resp = create_test_users(4) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] + + end + + after(:all) do + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Ban / Unban User + # --------------------------------------------------------------------------- + + describe 'BanUnbanUser' do + + it 'bans a user from a channel, verifies, and unbans' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + cid = "messaging:#{channel_id}" + + # Ban user in channel + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user_2, + banned_by_id: @user_1, + channel_cid: cid, + reason: 'moderation test ban', + timeout: 60, + ), + ) + + # Verify via query banned users + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + banned_user_ids = bans.map do |b| + + h = b.is_a?(Hash) ? b : b.to_h + target = h['user'] || {} + target = target.to_h unless target.is_a?(Hash) + target['id'] + + end + expect(banned_user_ids).to include(@user_2) + + # Unban user + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user_2, + cid, + ) + + # Verify ban is removed + payload = JSON.generate({ filter_conditions: { 'channel_cid' => { '$eq' => cid } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + banned_ids_after = bans_2.map do |b| + + h = b.is_a?(Hash) ? b : b.to_h + target = h['user'] || {} + target = target.to_h unless target.is_a?(Hash) + target['id'] + + end + expect(banned_ids_after).not_to include(@user_2) + + end + + it 'bans a user app-wide, verifies, and unbans' do + + # Ban user app-wide (no channel_cid) + @client.moderation.ban( + GetStream::Generated::Models::BanRequest.new( + target_user_id: @user_3, + banned_by_id: @user_1, + reason: 'app-wide moderation test ban', + timeout: 60, + ), + ) + + # Verify via query banned users (app-level) + payload = JSON.generate({ filter_conditions: { 'user_id' => { '$eq' => @user_3 } } }) + resp = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans = resp.bans || [] + expect(bans.length).to be >= 1 + + # Unban user app-wide + @client.moderation.unban( + GetStream::Generated::Models::UnbanRequest.new, + @user_3, + ) + + # Verify ban is removed + payload = JSON.generate({ filter_conditions: { 'user_id' => { '$eq' => @user_3 } } }) + resp_2 = @client.make_request( + :get, + '/api/v2/chat/query_banned_users', + query_params: { 'payload' => payload }, + ) + bans_2 = resp_2.bans || [] + expect(bans_2.length).to eq(0), 'App-wide ban should be removed after unban' + + end + + end + + # --------------------------------------------------------------------------- + # Mute / Unmute User + # --------------------------------------------------------------------------- + + describe 'MuteUnmuteUser' do + + it 'mutes a user, verifies via query, and unmutes' do + + # Mute user + mute_resp = @client.moderation.mute( + GetStream::Generated::Models::MuteRequest.new( + target_ids: [@user_4], + user_id: @user_1, + ), + ) + expect(mute_resp.mutes).not_to be_nil + expect(mute_resp.mutes.length).to be >= 1 + + mute_h = mute_resp.mutes[0].is_a?(Hash) ? mute_resp.mutes[0] : mute_resp.mutes[0].to_h + target = mute_h['target'] || {} + target = target.to_h unless target.is_a?(Hash) + expect(target['id']).to eq(@user_4) + + # Verify via QueryUsers that muter has mutes + q_resp = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) + expect(q_resp.users).not_to be_nil + expect(q_resp.users.length).to be >= 1 + user_h = q_resp.users[0].is_a?(Hash) ? q_resp.users[0] : q_resp.users[0].to_h + expect(user_h['mutes']).not_to be_nil + expect(user_h['mutes'].length).to be >= 1 + + muted_ids = user_h['mutes'].map do |m| + + t = m.is_a?(Hash) ? m : m.to_h + tgt = t['target'] || {} + tgt = tgt.to_h unless tgt.is_a?(Hash) + tgt['id'] + + end + expect(muted_ids).to include(@user_4) + + # Unmute user + @client.moderation.unmute( + GetStream::Generated::Models::UnmuteRequest.new( + target_ids: [@user_4], + user_id: @user_1, + ), + ) + + # Verify mute is removed + q_resp_2 = @client.common.query_users(JSON.generate({ + filter_conditions: { 'id' => { '$eq' => @user_1 } }, + })) + user_h_2 = q_resp_2.users[0].is_a?(Hash) ? q_resp_2.users[0] : q_resp_2.users[0].to_h + mutes_after = user_h_2['mutes'] || [] + muted_ids_after = mutes_after.map do |m| + + t = m.is_a?(Hash) ? m : m.to_h + tgt = t['target'] || {} + tgt = tgt.to_h unless tgt.is_a?(Hash) + tgt['id'] + + end + expect(muted_ids_after).not_to include(@user_4) + + end + + end + + # --------------------------------------------------------------------------- + # Flag Message and User + # --------------------------------------------------------------------------- + + describe 'FlagMessageAndUser' do + + it 'flags a message and verifies response' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Flaggable message #{SecureRandom.hex(4)}") + + # Flag message + flag_resp = @client.moderation.flag( + GetStream::Generated::Models::FlagRequest.new( + entity_type: 'stream:chat:v1:message', + entity_id: msg_id, + entity_creator_id: @user_1, + reason: 'inappropriate content', + user_id: @user_2, + ), + ) + expect(flag_resp).not_to be_nil + + end + + it 'flags a user and verifies response' do + + # Flag user + flag_resp = @client.moderation.flag( + GetStream::Generated::Models::FlagRequest.new( + entity_type: 'stream:user', + entity_id: @user_3, + entity_creator_id: @user_3, + reason: 'spam behavior', + user_id: @user_1, + ), + ) + expect(flag_resp).not_to be_nil + + end + + end + +end diff --git a/spec/integration/chat_polls_integration_spec.rb b/spec/integration/chat_polls_integration_spec.rb new file mode 100644 index 0000000..c41b388 --- /dev/null +++ b/spec/integration/chat_polls_integration_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Polls Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @shared_user_ids, _resp = create_test_users(2) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @created_poll_ids = [] + + end + + after(:all) do + + # Delete polls before channels/users (polls reference users) + @created_poll_ids&.each do |poll_id| + + @client.common.delete_poll(poll_id, @user_1) + rescue StandardError => e + puts "Warning: Failed to delete poll #{poll_id}: #{e.message}" + + end + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Poll API wrappers + # --------------------------------------------------------------------------- + + def create_poll(name, user_id, options: [], enforce_unique_vote: nil, description: nil) + poll_options = options.map do |text| + + GetStream::Generated::Models::PollOptionInput.new(text: text) + + end + + req = GetStream::Generated::Models::CreatePollRequest.new( + name: name, + user_id: user_id, + options: poll_options, + enforce_unique_vote: enforce_unique_vote, + description: description, + ) + + resp = @client.common.create_poll(req) + poll_id = resp.poll.id + @created_poll_ids << poll_id + resp + end + + def get_poll(poll_id) + @client.common.get_poll(poll_id) + end + + def query_polls(filter, user_id) + req = GetStream::Generated::Models::QueryPollsRequest.new(filter: filter) + @client.common.query_polls(req, user_id) + end + + def delete_poll(poll_id, user_id) + @client.common.delete_poll(poll_id, user_id) + end + + def cast_poll_vote(message_id, poll_id, user_id, option_id) + body = { + user_id: user_id, + vote: { option_id: option_id }, + } + @client.make_request( + :post, + "/api/v2/chat/messages/#{message_id}/polls/#{poll_id}/vote", + body: body, + ) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateAndQueryPoll' do + + it 'creates a poll with options, gets it, and queries it' do + + poll_name = "Favorite color? #{SecureRandom.hex(4)}" + + # Create poll with options + create_resp = create_poll( + poll_name, + @user_1, + options: %w[Red Blue Green], + enforce_unique_vote: true, + description: 'Pick your favorite color', + ) + expect(create_resp.poll).not_to be_nil + poll_id = create_resp.poll.id + expect(poll_id).not_to be_nil + expect(create_resp.poll.name).to eq(poll_name) + expect(create_resp.poll.enforce_unique_vote).to eq(true) + + poll_h = create_resp.poll.to_h + expect(poll_h['options'].length).to eq(3) + + # Get poll by ID + get_resp = get_poll(poll_id) + expect(get_resp.poll).not_to be_nil + expect(get_resp.poll.id).to eq(poll_id) + expect(get_resp.poll.name).to eq(poll_name) + + # Query polls with filter + query_resp = query_polls({ 'id' => poll_id }, @user_1) + expect(query_resp.polls).not_to be_nil + expect(query_resp.polls.length).to be >= 1 + + found = query_resp.polls.any? do |p| + + h = p.is_a?(Hash) ? p : p.to_h + h['id'] == poll_id + + end + expect(found).to be true + rescue StandardError => e + skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') + raise + + end + + end + + describe 'CastPollVote' do + + it 'creates a poll, attaches to message, casts vote, and verifies' do + + # Create poll + poll_name = "Vote test #{SecureRandom.hex(4)}" + create_resp = create_poll( + poll_name, + @user_1, + options: %w[Yes No], + enforce_unique_vote: true, + ) + poll_id = create_resp.poll.id + poll_h = create_resp.poll.to_h + option_id = poll_h['options'][0]['id'] + expect(option_id).not_to be_nil + + # Create channel with both users as members + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + + # Send message with poll attached + body = { + message: { + text: 'Please vote!', + user_id: @user_1, + poll_id: poll_id, + }, + } + msg_resp = send_message('messaging', channel_id, body) + msg_id = msg_resp.message.id + expect(msg_id).not_to be_nil + + # Cast a vote as user2 + vote_resp = cast_poll_vote(msg_id, poll_id, @user_2, option_id) + expect(vote_resp.vote).not_to be_nil + vote_h = vote_resp.vote.to_h + expect(vote_h['option_id']).to eq(option_id) + + # Verify poll has votes + get_resp = get_poll(poll_id) + expect(get_resp.poll.vote_count).to eq(1) + rescue StandardError => e + skip('Polls not enabled for this app') if e.message.include?('Polls') || e.message.include?('polls') + raise + + end + + end + +end diff --git a/spec/integration/chat_reaction_integration_spec.rb b/spec/integration/chat_reaction_integration_spec.rb new file mode 100644 index 0000000..d24db9a --- /dev/null +++ b/spec/integration/chat_reaction_integration_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat Reaction Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @shared_user_ids, _resp = create_test_users(2) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + + end + + after(:all) do + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Reaction API wrappers + # --------------------------------------------------------------------------- + + def send_reaction(message_id, reaction_type, user_id, enforce_unique: false) + body = { + reaction: { type: reaction_type, user_id: user_id }, + } + body[:enforce_unique] = true if enforce_unique + @client.make_request(:post, "/api/v2/chat/messages/#{message_id}/reaction", body: body) + end + + def get_reactions(message_id) + @client.make_request(:get, "/api/v2/chat/messages/#{message_id}/reactions") + end + + def delete_reaction(message_id, reaction_type, user_id) + @client.make_request( + :delete, + "/api/v2/chat/messages/#{message_id}/reaction/#{reaction_type}", + query_params: { 'user_id' => user_id }, + ) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'SendAndGetReactions' do + + it 'sends reactions and gets them back' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1, @user_2]) + msg_id = send_test_message('messaging', channel_id, @user_1, "React to this #{SecureRandom.hex(8)}") + + # Send two reactions from different users + resp_1 = send_reaction(msg_id, 'like', @user_1) + expect(resp_1.reaction).not_to be_nil + expect(resp_1.reaction.to_h['type']).to eq('like') + expect(resp_1.reaction.to_h['user_id']).to eq(@user_1) + + resp_2 = send_reaction(msg_id, 'love', @user_2) + expect(resp_2.reaction).not_to be_nil + expect(resp_2.reaction.to_h['type']).to eq('love') + expect(resp_2.reaction.to_h['user_id']).to eq(@user_2) + + # Get reactions + get_resp = get_reactions(msg_id) + expect(get_resp.reactions).not_to be_nil + expect(get_resp.reactions.length).to be >= 2 + + end + + end + + describe 'DeleteReaction' do + + it 'sends a reaction, deletes it, and verifies removal' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Delete reaction test #{SecureRandom.hex(8)}") + + # Send reaction + send_reaction(msg_id, 'like', @user_1) + + # Delete reaction + del_resp = delete_reaction(msg_id, 'like', @user_1) + expect(del_resp).not_to be_nil + + # Verify reaction is gone + get_resp = get_reactions(msg_id) + user_likes = (get_resp.reactions || []).select do |r| + + h = r.is_a?(Hash) ? r : r.to_h + h['user_id'] == @user_1 && h['type'] == 'like' + + end + expect(user_likes.length).to eq(0) + + end + + end + + describe 'EnforceUniqueReaction' do + + it 'enforces only one reaction per user when enforce_unique is set' do + + _type, channel_id, _resp = create_test_channel_with_members(@user_1, [@user_1]) + msg_id = send_test_message('messaging', channel_id, @user_1, "Unique reaction test #{SecureRandom.hex(8)}") + + # Send first reaction with enforce_unique + send_reaction(msg_id, 'like', @user_1, enforce_unique: true) + + # Send second reaction with enforce_unique — should replace, not duplicate + send_reaction(msg_id, 'love', @user_1, enforce_unique: true) + + # Verify user has only one reaction + get_resp = get_reactions(msg_id) + user_reactions = (get_resp.reactions || []).select do |r| + + h = r.is_a?(Hash) ? r : r.to_h + h['user_id'] == @user_1 + + end + expect(user_reactions.length).to eq(1) + + end + + end + +end diff --git a/spec/integration/chat_test_helpers.rb b/spec/integration/chat_test_helpers.rb new file mode 100644 index 0000000..79b151d --- /dev/null +++ b/spec/integration/chat_test_helpers.rb @@ -0,0 +1,188 @@ +# frozen_string_literal: true + +require 'securerandom' +require 'json' +require 'dotenv' +require_relative '../../lib/getstream_ruby' +require_relative 'suite_cleanup' + +# Shared helpers for chat integration tests. +# Include this module in RSpec describe blocks and call `init_chat_client` +# in a before(:all) hook. +module ChatTestHelpers + + # --------------------------------------------------------------------------- + # Setup / teardown + # --------------------------------------------------------------------------- + + def init_chat_client + Dotenv.load('.env') if File.exist?('.env') + @client = GetStreamRuby.client + @created_user_ids = [] + @created_channel_cids = [] + end + + def retry_on_rate_limit(max_attempts: 3) + attempts = 0 + begin + yield + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + attempts += 1 + raise if attempts >= max_attempts + + wait = 61 - Time.now.sec + puts "⏳ Rate-limited, waiting #{wait}s for window reset (attempt #{attempts}/#{max_attempts})..." + sleep(wait) + retry + end + end + + def cleanup_chat_resources + # Delete channels (they reference users and must be removed per-spec). + @created_channel_cids&.each do |cid| + + type, id = cid.split(':', 2) + retry_on_rate_limit do + + @client.make_request( + :delete, + "/api/v2/chat/channels/#{type}/#{id}", + query_params: { 'hard_delete' => 'true' }, + ) + + end + rescue StandardError => e + puts "Warning: Failed to delete channel #{cid}: #{e.message}" + + end + + # Register users for deferred deletion at suite end. + # The delete_users endpoint is rate-limited; batching all user deletes into + # a single after(:suite) call (in suite_cleanup.rb) keeps the quota free + # for the DeleteUsers integration test that runs during the suite. + SuiteCleanup.register_users(@created_user_ids) + end + + # --------------------------------------------------------------------------- + # Helper 1: random_string + # --------------------------------------------------------------------------- + + def random_string(length = 8) + SecureRandom.alphanumeric(length) + end + + # --------------------------------------------------------------------------- + # Helper 2: create_test_users + # --------------------------------------------------------------------------- + + def create_test_users(count) + ids = Array.new(count) { "test-user-#{SecureRandom.uuid}" } + users = {} + ids.each do |id| + + users[id] = GetStream::Generated::Models::UserRequest.new( + id: id, + name: "Test User #{id[0..7]}", + role: 'user', + ) + + end + + response = @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new(users: users), + ) + @created_user_ids.concat(ids) + [ids, response] + end + + # --------------------------------------------------------------------------- + # Helper 3: create_test_channel + # --------------------------------------------------------------------------- + + def create_test_channel(creator_id) + channel_id = "test-ch-#{SecureRandom.hex(6)}" + body = { data: { created_by_id: creator_id } } + response = @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: body, + ) + @created_channel_cids << "messaging:#{channel_id}" + ['messaging', channel_id, response] + end + + # --------------------------------------------------------------------------- + # Helper 4: create_test_channel_with_members + # --------------------------------------------------------------------------- + + def create_test_channel_with_members(creator_id, member_ids) + channel_id = "test-ch-#{SecureRandom.hex(6)}" + members = member_ids.map { |id| { user_id: id } } + body = { data: { created_by_id: creator_id, members: members } } + response = @client.make_request( + :post, + "/api/v2/chat/channels/messaging/#{channel_id}/query", + body: body, + ) + @created_channel_cids << "messaging:#{channel_id}" + ['messaging', channel_id, response] + end + + # --------------------------------------------------------------------------- + # Helper 5: send_test_message + # --------------------------------------------------------------------------- + + def send_test_message(channel_type, channel_id, user_id, text) + body = { message: { text: text, user_id: user_id } } + resp = @client.make_request( + :post, + "/api/v2/chat/channels/#{channel_type}/#{channel_id}/message", + body: body, + ) + resp.message.id + end + + # --------------------------------------------------------------------------- + # Helper 6: wait_for_task + # --------------------------------------------------------------------------- + + def wait_for_task(task_id, max_attempts: 60, interval_seconds: 1) + max_attempts.times do + + result = @client.common.get_task(task_id) + return result if %w[completed failed].include?(result.status) + + sleep(interval_seconds) + + end + raise "Task #{task_id} did not complete after #{max_attempts} attempts" + end + + # --------------------------------------------------------------------------- + # Channel API wrappers (for tests that need direct channel operations) + # --------------------------------------------------------------------------- + + def get_or_create_channel(type, id, body = {}) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/query", body: body) + end + + def delete_channel(type, id, hard: false) + query_params = hard ? { 'hard_delete' => 'true' } : {} + retry_on_rate_limit do + + @client.make_request(:delete, "/api/v2/chat/channels/#{type}/#{id}", query_params: query_params) + + end + end + + def query_channels(body) + @client.make_request(:post, '/api/v2/chat/channels', body: body) + end + + def send_message(type, id, body) + @client.make_request(:post, "/api/v2/chat/channels/#{type}/#{id}/message", body: body) + end + +end diff --git a/spec/integration/chat_user_group_integration_spec.rb b/spec/integration/chat_user_group_integration_spec.rb new file mode 100644 index 0000000..b2b61e6 --- /dev/null +++ b/spec/integration/chat_user_group_integration_spec.rb @@ -0,0 +1,285 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat User Group Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @created_group_ids = [] + + end + + after(:all) do + + @created_group_ids&.each do |gid| + + @client.common.delete_user_group(gid) + rescue StandardError => e + puts "Warning: Failed to delete user group #{gid}: #{e.message}" + + end + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Helper: create a group and track it for cleanup + # --------------------------------------------------------------------------- + + def create_group(id:, name:, description: nil, member_ids: nil) + req = GetStream::Generated::Models::CreateUserGroupRequest.new( + id: id, + name: name, + description: description, + member_ids: member_ids, + ) + resp = @client.common.create_user_group(req) + @created_group_ids << id + resp + rescue GetStreamRuby::APIError => e + skip 'User groups feature not available for this app' if e.message.include?('Not Found') + raise + end + + def delete_group(id) + @client.common.delete_user_group(id) + @created_group_ids.delete(id) + rescue StandardError + @created_group_ids.delete(id) + end + + # --------------------------------------------------------------------------- + # Tests + # --------------------------------------------------------------------------- + + describe 'CreateAndGetUserGroup' do + + it 'creates a group with name and description, then retrieves it by ID' do + + group_id = "test-group-#{SecureRandom.uuid}" + group_name = "Test Group #{group_id[0..14]}" + description = 'A test user group' + + create_resp = create_group(id: group_id, name: group_name, description: description) + expect(create_resp.user_group).not_to be_nil + expect(create_resp.user_group.id).to eq(group_id) + expect(create_resp.user_group.name).to eq(group_name) + expect(create_resp.user_group.description).to eq(description) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + expect(get_resp.user_group.id).to eq(group_id) + expect(get_resp.user_group.name).to eq(group_name) + + end + + end + + describe 'CreateUserGroupWithInitialMembers' do + + it 'creates a group with initial member IDs and verifies members are present' do + + user_ids, _resp = create_test_users(2) + group_id = "test-group-#{SecureRandom.uuid}" + + create_resp = create_group(id: group_id, name: 'Group With Members', member_ids: user_ids) + expect(create_resp.user_group).not_to be_nil + expect(create_resp.user_group.id).to eq(group_id) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + + members = get_resp.user_group.members || [] + found_ids = members.map { |m| m.is_a?(Hash) ? m['user_id'] : m.user_id } + user_ids.each do |uid| + + expect(found_ids).to include(uid) + + end + + end + + end + + describe 'UpdateUserGroup' do + + it 'updates the group name and description, then confirms via GET' do + + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: 'Original Name') + + new_name = 'Updated Name' + new_desc = 'Updated description' + update_resp = @client.common.update_user_group( + group_id, + GetStream::Generated::Models::UpdateUserGroupRequest.new( + name: new_name, + description: new_desc, + ), + ) + expect(update_resp.user_group).not_to be_nil + expect(update_resp.user_group.name).to eq(new_name) + expect(update_resp.user_group.description).to eq(new_desc) + + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + expect(get_resp.user_group.name).to eq(new_name) + + end + + end + + describe 'ListUserGroups' do + + it 'lists groups and at least one created group appears' do + + group_id_a = "test-group-#{SecureRandom.uuid}" + group_id_b = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id_a, name: 'List Test Group One') + create_group(id: group_id_b, name: 'List Test Group Two') + + list_resp = @client.common.list_user_groups + expect(list_resp.user_groups).not_to be_nil + expect(list_resp.user_groups).not_to be_empty + + found_ids = list_resp.user_groups.map { |g| g.is_a?(Hash) ? g['id'] : g.id } + expect(found_ids).to include(group_id_a).or include(group_id_b) + + end + + end + + describe 'ListUserGroupsWithLimit' do + + it 'respects the limit parameter' do + + group_ids = Array.new(3) { "test-group-#{SecureRandom.uuid}" } + group_ids.each_with_index do |gid, i| + + create_group(id: gid, name: "Limit Test Group #{i + 1}") + + end + + limit = 2 + list_resp = @client.common.list_user_groups(limit) + expect(list_resp.user_groups).not_to be_nil + expect(list_resp.user_groups.length).to be <= limit + + end + + end + + describe 'SearchUserGroups' do + + it 'finds a group by name prefix search' do + + unique_prefix = "SearchTest-#{SecureRandom.hex(4)}" + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: "#{unique_prefix} Group") + + search_resp = @client.common.search_user_groups(unique_prefix) + expect(search_resp.user_groups).not_to be_nil + + found = search_resp.user_groups.any? do |g| + + name = g.is_a?(Hash) ? g['name'] : g.name + name.to_s.start_with?(unique_prefix) + + end + expect(found).to be true + + end + + end + + describe 'AddUserGroupMembers' do + + it 'adds members to an existing group and verifies all are present' do + + user_ids, _resp = create_test_users(3) + group_id = "test-group-#{SecureRandom.uuid}" + + # Create with first member only + create_group(id: group_id, name: 'Member Management Group', member_ids: user_ids[0, 1]) + + # Add remaining members + add_resp = @client.common.add_user_group_members( + group_id, + GetStream::Generated::Models::AddUserGroupMembersRequest.new( + member_ids: user_ids[1..], + ), + ) + expect(add_resp.user_group).not_to be_nil + + # Verify all members are present + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group).not_to be_nil + + members = get_resp.user_group.members || [] + found_ids = members.map { |m| m.is_a?(Hash) ? m['user_id'] : m.user_id } + user_ids.each do |uid| + + expect(found_ids).to include(uid) + + end + + end + + end + + describe 'RemoveUserGroupMembers' do + + it 'removes all members from a group and verifies it is empty' do + + user_ids, _resp = create_test_users(2) + group_id = "test-group-#{SecureRandom.uuid}" + + # Create group with members + create_group(id: group_id, name: 'Remove Members Group', member_ids: user_ids) + + # Verify members are present before removal + get_resp = @client.common.get_user_group(group_id) + expect(get_resp.user_group.members.length).to eq(user_ids.length) + + # Remove all members explicitly by ID (backend requires member_ids) + @client.common.remove_user_group_members( + group_id, + GetStream::Generated::Models::RemoveUserGroupMembersRequest.new( + member_ids: user_ids, + ), + ) + + # Verify members are removed + get_resp_after = @client.common.get_user_group(group_id) + expect(get_resp_after.user_group).not_to be_nil + members_after = get_resp_after.user_group.members + expect(members_after).to satisfy('be nil or empty') { |m| m.nil? || m.empty? } + + end + + end + + describe 'DeleteUserGroup' do + + it 'deletes a group and verifies a subsequent GET returns an error' do + + group_id = "test-group-#{SecureRandom.uuid}" + create_group(id: group_id, name: 'Group To Delete') + + delete_group(group_id) + + expect { @client.common.get_user_group(group_id) }.to raise_error(StandardError) + + end + + end + +end diff --git a/spec/integration/chat_user_integration_spec.rb b/spec/integration/chat_user_integration_spec.rb new file mode 100644 index 0000000..7d3aeae --- /dev/null +++ b/spec/integration/chat_user_integration_spec.rb @@ -0,0 +1,563 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Chat User Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + + end + + after(:all) do + + cleanup_chat_resources + + end + + # Helper to query users with a filter + def query_users_with_filter(filter, **opts) + payload = { 'filter_conditions' => filter } + payload['limit'] = opts[:limit] if opts[:limit] + payload['offset'] = opts[:offset] if opts[:offset] + payload['include_deactivated_users'] = opts[:include_deactivated_users] if opts.key?(:include_deactivated_users) + payload['sort'] = opts[:sort] if opts[:sort] + @client.common.query_users(JSON.generate(payload)) + end + + describe 'UpsertUsers' do + + it 'creates 2 users and verifies both in response' do + + user_ids, response = create_test_users(2) + + expect(response).to be_a(GetStreamRuby::StreamResponse) + expect(user_ids.length).to eq(2) + + users_hash = response.users + expect(users_hash).not_to be_nil + user_ids.each do |uid| + + expect(users_hash.to_h.key?(uid)).to be true + + end + + end + + end + + describe 'QueryUsers' do + + it 'queries users with $in filter and verifies found' do + + user_ids, _resp = create_test_users(2) + + resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(resp.users).not_to be_nil + expect(resp.users.length).to be >= 2 + + returned_ids = resp.users.map { |u| u.to_h['id'] || u.id } + user_ids.each do |uid| + + expect(returned_ids).to include(uid) + + end + + end + + end + + describe 'QueryUsersWithOffsetLimit' do + + it 'queries with offset=1 limit=2 and verifies exactly 2 returned' do + + user_ids, _resp = create_test_users(3) + + resp = query_users_with_filter( + { 'id' => { '$in' => user_ids } }, + offset: 1, + limit: 2, + sort: [{ 'field' => 'id', 'direction' => 1 }], + ) + expect(resp.users).not_to be_nil + expect(resp.users.length).to eq(2) + + end + + end + + describe 'PartialUpdateUser' do + + it 'sets custom fields then unsets one' do + + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # Set country and role + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { 'country' => 'NL', 'role' => 'admin' }, + ), + ], + ), + ) + + # Verify set + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + # Custom fields may be at top-level or under 'custom' + country = user_h['custom'].is_a?(Hash) ? user_h['custom']['country'] : user_h['country'] + expect(country).to eq('NL') + + # Unset country + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + unset: ['country'], + ), + ], + ), + ) + + # Verify unset + resp_2 = query_users_with_filter({ 'id' => uid }) + user_2 = resp_2.users.first + user_2_hash = user_2.to_h + country_2 = user_2_hash['custom'].is_a?(Hash) ? user_2_hash['custom']['country'] : user_2_hash['country'] + expect(country_2).to be_nil + + end + + end + + describe 'BlockUnblockUser' do + + it 'blocks user, verifies in blocked list, unblocks, verifies removed' do + + user_ids, _resp = create_test_users(2) + blocker_id = user_ids[0] + blocked_id = user_ids[1] + + # Block + @client.common.block_users( + GetStream::Generated::Models::BlockUsersRequest.new( + blocked_user_id: blocked_id, + user_id: blocker_id, + ), + ) + + # Verify blocked + blocked_resp = @client.common.get_blocked_users(blocker_id) + expect(blocked_resp.blocks).not_to be_nil + blocked_user_ids = blocked_resp.blocks.map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } + expect(blocked_user_ids).to include(blocked_id) + + # Unblock + @client.common.unblock_users( + GetStream::Generated::Models::UnblockUsersRequest.new( + blocked_user_id: blocked_id, + user_id: blocker_id, + ), + ) + + # Verify unblocked + blocked_resp_2 = @client.common.get_blocked_users(blocker_id) + blocked_user_ids_2 = (blocked_resp_2.blocks || []).map { |b| b.to_h['blocked_user_id'] || b.blocked_user_id } + expect(blocked_user_ids_2).not_to include(blocked_id) + + end + + end + + describe 'DeactivateReactivateUser' do + + it 'deactivates then reactivates a user' do + + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # Deactivate + @client.common.deactivate_user( + uid, + GetStream::Generated::Models::DeactivateUserRequest.new, + ) + + # Reactivate + @client.common.reactivate_user( + uid, + GetStream::Generated::Models::ReactivateUserRequest.new, + ) + + # Verify active by querying + resp = query_users_with_filter({ 'id' => uid }) + expect(resp.users.length).to eq(1) + + end + + end + + describe 'DeleteUsers' do + + it 'deletes 2 users and polls task until completed' do + + user_ids, _resp = create_test_users(2) + + resp = nil + 3.times do + + resp = @client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + messages: 'hard', + conversations: 'hard', + ), + ) + # Remove from tracked list only after a successful call so that a + # rate-limit failure on a prior attempt still leaves them registered + # for suite cleanup. + user_ids.each { |uid| @created_user_ids.delete(uid) } + break + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + wait = 61 - Time.now.sec + sleep(wait) + + end + + expect(resp).not_to be_nil + task_id = resp.task_id + expect(task_id).not_to be_nil + + result = wait_for_task(task_id) + expect(result.status).to eq('completed') + + end + + end + + describe 'ExportUser' do + + it 'exports a user and verifies response not nil' do + + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + resp = @client.common.export_user(uid) + expect(resp).not_to be_nil + + end + + end + + describe 'CreateGuest' do + + it 'creates guest and verifies access token' do + + guest_id = "test-guest-#{SecureRandom.uuid}" + + resp = @client.common.create_guest( + GetStream::Generated::Models::CreateGuestRequest.new( + user: GetStream::Generated::Models::UserRequest.new( + id: guest_id, + name: 'Test Guest', + ), + ), + ) + + expect(resp.access_token).not_to be_nil + expect(resp.access_token).not_to be_empty + + # Clean up the guest user + @created_user_ids << guest_id + rescue GetStreamRuby::APIError => e + skip('Guest access not enabled') if e.message.downcase.include?('guest') + raise + + end + + end + + describe 'UpsertUsersWithRoleAndTeamsRole' do + + it 'creates user with role=admin, teams, and teams_role' do + + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Admin User #{uid[0..7]}", + role: 'admin', + teams: ['blue'], + teams_role: { 'blue' => 'admin' }, + ), + }, + ), + ) + + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + expect(user_h['role']).to eq('admin') + expect(user_h['teams']).to include('blue') + expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + + end + + end + + describe 'PartialUpdateUserWithTeam' do + + it 'partial updates to add teams and teams_role' do + + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'teams' => ['blue'], + 'teams_role' => { 'blue' => 'admin' }, + }, + ), + ], + ), + ) + + resp = query_users_with_filter({ 'id' => uid }) + user = resp.users.first + user_h = user.to_h + expect(user_h['teams']).to include('blue') + expect(user_h['teams_role']).to eq({ 'blue' => 'admin' }) + + end + + end + + describe 'UpdatePrivacySettings' do + + it 'sets typing_indicators disabled then sets both typing + read_receipts' do + + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + # Create user with typing_indicators disabled + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Privacy User #{uid[0..7]}", + privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( + typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: false), + ), + ), + }, + ), + ) + + resp = query_users_with_filter({ 'id' => uid }) + user_h = resp.users.first.to_h + expect(user_h.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(false) + + # Update both typing_indicators and read_receipts + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + privacy_settings: GetStream::Generated::Models::PrivacySettingsResponse.new( + typing_indicators: GetStream::Generated::Models::TypingIndicatorsResponse.new(enabled: true), + read_receipts: GetStream::Generated::Models::ReadReceiptsResponse.new(enabled: false), + ), + ), + }, + ), + ) + + resp_2 = query_users_with_filter({ 'id' => uid }) + user_h_2 = resp_2.users.first.to_h + expect(user_h_2.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) + expect(user_h_2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + + end + + end + + describe 'PartialUpdatePrivacySettings' do + + it 'partial updates privacy settings incrementally' do + + user_ids, _resp = create_test_users(1) + uid = user_ids.first + + # First: set typing_indicators.enabled = true + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'privacy_settings' => { + 'typing_indicators' => { 'enabled' => true }, + }, + }, + ), + ], + ), + ) + + resp = query_users_with_filter({ 'id' => uid }) + user_h = resp.users.first.to_h + expect(user_h.dig('privacy_settings', 'typing_indicators', 'enabled')).to eq(true) + + # Second: set read_receipts.enabled = false + @client.common.update_users_partial( + GetStream::Generated::Models::UpdateUsersPartialRequest.new( + users: [ + GetStream::Generated::Models::UpdateUserPartialRequest.new( + id: uid, + set: { + 'privacy_settings' => { + 'read_receipts' => { 'enabled' => false }, + }, + }, + ), + ], + ), + ) + + resp_2 = query_users_with_filter({ 'id' => uid }) + user_h_2 = resp_2.users.first.to_h + expect(user_h_2.dig('privacy_settings', 'read_receipts', 'enabled')).to eq(false) + + end + + end + + describe 'QueryUsersWithDeactivated' do + + it 'deactivates one user, queries without/with include_deactivated' do + + user_ids, _resp = create_test_users(3) + deactivated_id = user_ids.first + + # Deactivate one user + @client.common.deactivate_user( + deactivated_id, + GetStream::Generated::Models::DeactivateUserRequest.new, + ) + + # Query WITHOUT include_deactivated_users — expect 2 + resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(resp.users.length).to eq(2) + + # Query WITH include_deactivated_users — expect 3 + resp_2 = query_users_with_filter( + { 'id' => { '$in' => user_ids } }, + include_deactivated_users: true, + ) + expect(resp_2.users.length).to eq(3) + + # Reactivate for cleanup + @client.common.reactivate_user( + deactivated_id, + GetStream::Generated::Models::ReactivateUserRequest.new, + ) + + end + + end + + describe 'DeactivateUsersPlural' do + + it 'deactivates multiple users at once via async task' do + + user_ids, _resp = create_test_users(2) + + resp = @client.common.deactivate_users( + GetStream::Generated::Models::DeactivateUsersRequest.new( + user_ids: user_ids, + ), + ) + + task_id = resp.task_id + expect(task_id).not_to be_nil + + result = wait_for_task(task_id) + expect(result.status).to eq('completed') + + # Verify deactivated users don't appear in default query + query_resp = query_users_with_filter({ 'id' => { '$in' => user_ids } }) + expect(query_resp.users.length).to eq(0) + + # Reactivate for cleanup + user_ids.each do |uid| + + @client.common.reactivate_user(uid, GetStream::Generated::Models::ReactivateUserRequest.new) + + end + + end + + end + + describe 'UserCustomData' do + + it 'creates user with custom fields and verifies persistence' do + + uid = "test-user-#{SecureRandom.uuid}" + @created_user_ids << uid + + custom_data = { + 'favorite_color' => 'blue', + 'age' => 30, + 'tags' => %w[vip early_adopter], + } + + resp = @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + uid => GetStream::Generated::Models::UserRequest.new( + id: uid, + name: "Custom User #{uid[0..7]}", + custom: custom_data, + ), + }, + ), + ) + + # Verify in upsert response + users_hash = resp.users.to_h + expect(users_hash).to have_key(uid) + + # Verify via query + query_resp = query_users_with_filter({ 'id' => uid }) + user_h = query_resp.users.first.to_h + expect(user_h['custom']['favorite_color'] || user_h['favorite_color']).to eq('blue') + + end + + end + +end diff --git a/spec/integration/feed_integration_spec.rb b/spec/integration/feed_integration_spec.rb index 05b2ea0..3c8e9b9 100644 --- a/spec/integration/feed_integration_spec.rb +++ b/spec/integration/feed_integration_spec.rb @@ -310,20 +310,8 @@ expect(response).to be_a(GetStreamRuby::StreamResponse) puts "✅ Created/updated users in batch: #{user_id_1}, #{user_id_2}" # snippet-stop: UpdateUsers - - # Wait for backend propagation - test_helper.wait_for_backend_propagation(1) ensure - # Cleanup created users - begin - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: [user_id_1, user_id_2], - user: 'hard', - ) - client.common.delete_users(delete_request) - rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" - end + SuiteCleanup.register_users([user_id_1, user_id_2]) end end @@ -346,7 +334,6 @@ }, ) client.common.update_users(create_request) - test_helper.wait_for_backend_propagation(1) # snippet-start: UpdateUsersPartial # Partially update user @@ -366,16 +353,7 @@ puts "✅ Partially updated user: #{user_id}" # snippet-stop: UpdateUsersPartial ensure - # Cleanup - begin - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: [user_id], - user: 'hard', - ) - client.common.delete_users(delete_request) - rescue StandardError => e - puts "⚠️ Cleanup error: #{e.message}" - end + SuiteCleanup.register_users([user_id]) end end @@ -406,32 +384,43 @@ create_request = GetStream::Generated::Models::UpdateUsersRequest.new(users: users_hash) client.common.update_users(create_request) - test_helper.wait_for_backend_propagation(1) # snippet-start: DeleteUsers - # Delete users in batch - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', - ) + # Delete users in batch. + # delete_users is rate-limited to 6 req/min on a fixed 1-minute clock + # window. Rejected 429 calls still increment the counter, so arbitrary + # backoff makes recovery harder. On a 429, sleep until the next minute + # boundary (at most 61s) to guarantee a fresh window before retrying. + response = nil + last_error = nil + 3.times do + + last_error = nil + response = client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: user_ids, + user: 'hard', + ), + ) + break + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + last_error = e + sleep(61 - Time.now.sec) + + end - response = client.common.delete_users(delete_request) + raise last_error if last_error + + expect(response).not_to be_nil expect(response).to be_a(GetStreamRuby::StreamResponse) puts "✅ Deleted #{user_ids.length} users in batch" # snippet-stop: DeleteUsers - rescue StandardError => e - puts "⚠️ Error: #{e.message}" - # Try cleanup anyway - begin - delete_request = GetStream::Generated::Models::DeleteUsersRequest.new( - user_ids: user_ids, - user: 'hard', - ) - client.common.delete_users(delete_request) - rescue StandardError - # Ignore cleanup errors - end - raise e + ensure + # Register for suite cleanup. If delete_users already succeeded above, + # the suite cleanup attempt on these IDs is a harmless no-op. + SuiteCleanup.register_users(user_ids) end end @@ -832,30 +821,36 @@ puts "\n📤 Testing file upload..." - # Get the path to the test file (in the same directory as the spec) - test_file_path = File.join(__dir__, 'upload-test.png') - raise "Test file not found: #{test_file_path}" unless File.exist?(test_file_path) + # Create a temporary text file (feed API upload_file supports text, not images) + require 'tempfile' + tmpfile = Tempfile.new(['feed-upload-test-', '.txt']) + tmpfile.write('hello world test file content from Ruby SDK') + tmpfile.close - # Create file upload request - file_upload_request = GetStream::Generated::Models::FileUploadRequest.new( - file: test_file_path, - user: GetStream::Generated::Models::OnlyUserID.new(id: test_user_id_1), - ) + begin + # Create file upload request + file_upload_request = GetStream::Generated::Models::FileUploadRequest.new( + file: tmpfile.path, + user: GetStream::Generated::Models::OnlyUserID.new(id: test_user_id_1), + ) - # Upload the file - upload_response = client.common.upload_file(file_upload_request) + # Upload the file + upload_response = client.common.upload_file(file_upload_request) - expect(upload_response).to be_a(GetStreamRuby::StreamResponse) - expect(upload_response.file).not_to be_nil - expect(upload_response.file).to be_a(String) - expect(upload_response.file).not_to be_empty + expect(upload_response).to be_a(GetStreamRuby::StreamResponse) + expect(upload_response.file).not_to be_nil + expect(upload_response.file).to be_a(String) + expect(upload_response.file).not_to be_empty - puts '✅ File uploaded successfully' - puts " File URL: #{upload_response.file}" - puts " Thumbnail URL: #{upload_response.thumb_url}" if upload_response.thumb_url + puts '✅ File uploaded successfully' + puts " File URL: #{upload_response.file}" + puts " Thumbnail URL: #{upload_response.thumb_url}" if upload_response.thumb_url - # Verify the URL is a valid URL - expect(upload_response.file).to match(/^https?:\/\//) + # Verify the URL is a valid URL + expect(upload_response.file).to match(/^https?:\/\//) + ensure + tmpfile.unlink + end end diff --git a/spec/integration/suite_cleanup.rb b/spec/integration/suite_cleanup.rb new file mode 100644 index 0000000..f4eb796 --- /dev/null +++ b/spec/integration/suite_cleanup.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'dotenv' + +# Global registry that collects test user IDs across all spec files and +# deletes them in a single batched call after the full suite completes. +# +# Why: the delete_users endpoint is rate-limited. Calling it once per spec +# file (8+ calls per run) exhausts the quota and causes the DeleteUsers +# integration test to fail. Batching everything into one call at suite end +# reduces pressure to 1–2 API calls per run and keeps the test data clean. +# +# Usage: +# SuiteCleanup.register_users(user_ids) # call from any spec/helper +# +# The after(:suite) hook below triggers the actual deletion automatically. +module SuiteCleanup + + @user_ids = [] + + class << self + + def register_users(ids) + @user_ids.concat(Array(ids).compact) + end + + def run + return if @user_ids.empty? + + Dotenv.load('.env') if File.exist?('.env') + + # Require the library; it may already be loaded, require is idempotent. + require_relative '../../lib/getstream_ruby' + + # Allow network access in case WebMock disabled it after the last test. + WebMock.allow_net_connect! if defined?(WebMock) + + client = GetStreamRuby.client + uniq_ids = @user_ids.uniq + puts "\n🧹 Suite cleanup: deleting #{uniq_ids.length} test users..." + + # The delete_users endpoint accepts up to 100 user IDs per request. + uniq_ids.each_slice(100) do |batch| + + 3.times do + + client.common.delete_users( + GetStream::Generated::Models::DeleteUsersRequest.new( + user_ids: batch, + user: 'hard', + messages: 'hard', + conversations: 'hard', + ), + ) + break + rescue GetStreamRuby::APIError => e + raise unless e.message.include?('Too many requests') + + # The Stream backend enforces 6 req/min on a fixed 1-minute clock + # window. Sleep until the next boundary (at most 61s) to guarantee + # a fresh window. Arbitrary backoff risks retrying in the same window. + wait = 61 - Time.now.sec + puts "⏳ Rate-limited during suite cleanup, waiting #{wait}s for window reset..." + sleep(wait) + + end + + end + + puts '✅ Suite cleanup complete' + end + + end + +end + +RSpec.configure do |config| + + config.after(:suite) do + + SuiteCleanup.run + + end + +end diff --git a/spec/integration/video_integration_spec.rb b/spec/integration/video_integration_spec.rb new file mode 100644 index 0000000..9f2b101 --- /dev/null +++ b/spec/integration/video_integration_spec.rb @@ -0,0 +1,838 @@ +# frozen_string_literal: true + +require 'rspec' +require 'securerandom' +require 'json' +require_relative 'chat_test_helpers' + +RSpec.describe 'Video Integration', type: :integration do + + include ChatTestHelpers + + before(:all) do + + init_chat_client + @created_call_type_names = [] + @created_call_ids = [] # [call_type, call_id] pairs + @shared_user_ids, _resp = create_test_users(4) + @user_1 = @shared_user_ids[0] + @user_2 = @shared_user_ids[1] + @user_3 = @shared_user_ids[2] + @user_4 = @shared_user_ids[3] + + end + + after(:all) do + + # Clean up calls (soft delete) + @created_call_ids&.each do |call_type, call_id| + + @client.make_request( + :post, + "/api/v2/video/call/#{call_type}/#{call_id}/delete", + body: {}, + ) + rescue StandardError => e + puts "Warning: Failed to delete call #{call_type}:#{call_id}: #{e.message}" + + end + + # Clean up call types + @created_call_type_names&.each do |name| + + @client.make_request(:delete, "/api/v2/video/calltypes/#{name}") + rescue StandardError => e + puts "Warning: Failed to delete call type #{name}: #{e.message}" + + end + + cleanup_chat_resources + + end + + # --------------------------------------------------------------------------- + # Helpers + # --------------------------------------------------------------------------- + + def create_call(call_type, call_id, body = {}) + @client.make_request(:post, "/api/v2/video/call/#{call_type}/#{call_id}", body: body) + end + + def get_call(call_type, call_id) + @client.make_request(:get, "/api/v2/video/call/#{call_type}/#{call_id}") + end + + def update_call(call_type, call_id, body) + @client.make_request(:patch, "/api/v2/video/call/#{call_type}/#{call_id}", body: body) + end + + def delete_call_req(call_type, call_id, body = {}) + @client.make_request(:post, "/api/v2/video/call/#{call_type}/#{call_id}/delete", body: body) + end + + def new_call_id + "test-call-#{random_string(10)}" + end + + def new_call_type_name + "testct#{random_string(8)}" + end + + # --------------------------------------------------------------------------- + # CRUDCallTypeOperations + # --------------------------------------------------------------------------- + + describe 'CRUDCallTypeOperations' do + + it 'creates a call type with settings, updates, reads, and deletes' do + + ct_name = new_call_type_name + @created_call_type_names << ct_name + + # Create call type + resp = @client.make_request(:post, '/api/v2/video/calltypes', body: { + name: ct_name, + grants: { + 'admin' => %w[send-audio send-video mute-users], + 'user' => %w[send-audio send-video], + }, + settings: { + audio: { default_device: 'speaker', mic_default_on: true }, + screensharing: { access_request_enabled: false, enabled: true }, + }, + notification_settings: { + enabled: true, + call_notification: { + enabled: true, + apns: { title: '{{ user.display_name }} invites you to a call', body: '' }, + }, + session_started: { enabled: false }, + call_live_started: { enabled: false }, + call_ring: { enabled: false }, + }, + }) + expect(resp.name).to eq(ct_name) + + # Poll for eventual consistency + 10.times do + + @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") + break + rescue GetStreamRuby::APIError + sleep(1) + + end + + # Update call type settings (with retry for eventual consistency) + resp_2 = nil + 3.times do |i| + + resp_2 = @client.make_request(:put, "/api/v2/video/calltypes/#{ct_name}", body: { + settings: { + audio: { default_device: 'earpiece', mic_default_on: false }, + recording: { mode: 'disabled' }, + backstage: { enabled: true }, + }, + grants: { + 'host' => %w[join-backstage], + }, + }) + break + rescue GetStreamRuby::APIError + raise if i == 2 + + sleep(2) + + end + expect(resp_2).not_to be_nil + + # Read call type (with retry) + resp_3 = nil + 3.times do |i| + + resp_3 = @client.make_request(:get, "/api/v2/video/calltypes/#{ct_name}") + break + rescue GetStreamRuby::APIError + raise if i == 2 + + sleep(2) + + end + expect(resp_3.name).to eq(ct_name) + + # Delete call type (with retry for eventual consistency) + sleep(2) + 5.times do |i| + + @client.make_request(:delete, "/api/v2/video/calltypes/#{ct_name}") + @created_call_type_names.delete(ct_name) + break + rescue GetStreamRuby::APIError + raise if i == 4 + + sleep(2) + + end + + end + + end + + # --------------------------------------------------------------------------- + # CreateCallWithMembers + # --------------------------------------------------------------------------- + + describe 'CreateCallWithMembers' do + + it 'creates a call and adds members' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: @user_1, + members: [ + { user_id: @user_1 }, + { user_id: @user_2 }, + ], + }, + }) + expect(resp).not_to be_nil + call_h = resp.to_h + expect(call_h['call']).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # BlockUnblockUserFromCalls + # --------------------------------------------------------------------------- + + describe 'BlockUnblockUserFromCalls' do + + it 'blocks a user from a call and unblocks' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + # Block user + @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/block", + body: { user_id: @user_2 }, + ) + + # Verify blocked + resp = get_call('default', call_id) + call_h = resp.to_h + blocked_ids = call_h.dig('call', 'blocked_user_ids') || [] + expect(blocked_ids).to include(@user_2) + + # Unblock user + @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/unblock", + body: { user_id: @user_2 }, + ) + + # Verify unblocked (with retry for eventual consistency) + unblocked = false + 5.times do + + sleep(1) + resp_2 = get_call('default', call_id) + call_h_2 = resp_2.to_h + blocked_ids_2 = call_h_2.dig('call', 'blocked_user_ids') || [] + unless blocked_ids_2.include?(@user_2) + unblocked = true + break + end + + end + expect(unblocked).to be(true), 'Expected user to be unblocked after unblock call' + + end + + end + + # --------------------------------------------------------------------------- + # SendCustomEvent + # --------------------------------------------------------------------------- + + describe 'SendCustomEvent' do + + it 'sends a custom event in a call' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/event", + body: { user_id: @user_1, custom: { bananas: 'good' } }, + ) + expect(resp).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # MuteAll + # --------------------------------------------------------------------------- + + describe 'MuteAll' do + + it 'mutes all users in a call' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/mute_users", + body: { + muted_by_id: @user_1, + mute_all_users: true, + audio: true, + }, + ) + expect(resp).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # MuteSomeUsers + # --------------------------------------------------------------------------- + + describe 'MuteSomeUsers' do + + it 'mutes specific users with audio, video, screenshare' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + resp = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/mute_users", + body: { + muted_by_id: @user_1, + user_ids: [@user_2, @user_3], + audio: true, + video: true, + screenshare: true, + screenshare_audio: true, + }, + ) + expect(resp).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # UpdateUserPermissions + # --------------------------------------------------------------------------- + + describe 'UpdateUserPermissions' do + + it 'revokes and grants permissions in a call' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + # Revoke send-audio + resp_1 = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/user_permissions", + body: { + user_id: @user_2, + revoke_permissions: ['send-audio'], + }, + ) + expect(resp_1).not_to be_nil + + # Grant send-audio back + resp_2 = @client.make_request( + :post, + "/api/v2/video/call/default/#{call_id}/user_permissions", + body: { + user_id: @user_2, + grant_permissions: ['send-audio'], + }, + ) + expect(resp_2).not_to be_nil + + end + + end + + # --------------------------------------------------------------------------- + # DeactivateUser (video context: deactivate/reactivate/batch) + # --------------------------------------------------------------------------- + + describe 'DeactivateUser' do + + it 'deactivates, reactivates, and batch deactivates users' do + + user_ids, _resp = create_test_users(2) + alice = user_ids[0] + bob = user_ids[1] + + # Deactivate single user + @client.common.deactivate_user( + alice, + GetStream::Generated::Models::DeactivateUserRequest.new, + ) + + # Reactivate single user + @client.common.reactivate_user( + alice, + GetStream::Generated::Models::ReactivateUserRequest.new, + ) + + # Batch deactivate + resp = @client.common.deactivate_users( + GetStream::Generated::Models::DeactivateUsersRequest.new(user_ids: [alice, bob]), + ) + expect(resp.task_id).not_to be_nil + + task_result = wait_for_task(resp.task_id) + expect(task_result.status).to eq('completed') + + end + + end + + # --------------------------------------------------------------------------- + # CreateCallWithSessionTimer + # --------------------------------------------------------------------------- + + describe 'CreateCallWithSessionTimer' do + + it 'creates a call with max_duration_seconds and updates it' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: @user_1, + settings_override: { + limits: { max_duration_seconds: 3600 }, + }, + }, + }) + call_h = resp.to_h + max_dur = call_h.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur).to eq(3600) + + # Update to 7200 + resp_2 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 7200 }, + }, + }) + call_h_2 = resp_2.to_h + max_dur_2 = call_h_2.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur_2).to eq(7200) + + # Reset to 0 + resp_3 = update_call('default', call_id, { + settings_override: { + limits: { max_duration_seconds: 0 }, + }, + }) + call_h_3 = resp_3.to_h + max_dur_3 = call_h_3.dig('call', 'settings', 'limits', 'max_duration_seconds') + expect(max_dur_3).to eq(0) + + end + + end + + # --------------------------------------------------------------------------- + # UserBlocking (app-level user block/unblock, not call-level) + # --------------------------------------------------------------------------- + + describe 'UserBlocking' do + + it 'blocks and unblocks a user at app level' do + + user_ids, _resp = create_test_users(2) + alice = user_ids[0] + bob = user_ids[1] + + # Block + @client.common.block_users( + GetStream::Generated::Models::BlockUsersRequest.new( + blocked_user_id: bob, + user_id: alice, + ), + ) + + # Verify blocked + resp = @client.common.get_blocked_users(alice) + blocks = resp.blocks || [] + expect(blocks.length).to be >= 1 + block_h = blocks[0].is_a?(Hash) ? blocks[0] : blocks[0].to_h + expect(block_h['blocked_user_id']).to eq(bob) + + # Unblock + @client.common.unblock_users( + GetStream::Generated::Models::UnblockUsersRequest.new( + blocked_user_id: bob, + user_id: alice, + ), + ) + + # Verify unblocked + resp_2 = @client.common.get_blocked_users(alice) + blocks_2 = resp_2.blocks || [] + blocked_ids = blocks_2.map do |b| + + h = b.is_a?(Hash) ? b : b.to_h + h['blocked_user_id'] + + end + expect(blocked_ids).not_to include(bob) + + end + + end + + # --------------------------------------------------------------------------- + # CreateCallWithBackstageAndJoinAhead + # --------------------------------------------------------------------------- + + describe 'CreateCallWithBackstageAndJoinAhead' do + + it 'creates a call with backstage and join_ahead_time_seconds' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + starts_at = (Time.now.utc + (30 * 60)).strftime('%Y-%m-%dT%H:%M:%S.%NZ') + + resp = create_call('default', call_id, { + data: { + starts_at: starts_at, + created_by_id: @user_1, + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 300 }, + }, + }, + }) + call_h = resp.to_h + join_ahead = call_h.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead).to eq(300) + + # Update to 600 + resp_2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 600 }, + }, + }) + call_h_2 = resp_2.to_h + join_ahead_2 = call_h_2.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead_2).to eq(600) + + # Reset to 0 + resp_3 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true, join_ahead_time_seconds: 0 }, + }, + }) + call_h_3 = resp_3.to_h + join_ahead_3 = call_h_3.dig('call', 'settings', 'backstage', 'join_ahead_time_seconds') + expect(join_ahead_3).to eq(0) + + end + + end + + # --------------------------------------------------------------------------- + # DeleteCall (soft) + # --------------------------------------------------------------------------- + + describe 'DeleteCall (soft)' do + + it 'soft deletes a call and verifies not found' do + + call_id = new_call_id + # Don't add to @created_call_ids since we're deleting it here + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + resp = delete_call_req('default', call_id, {}) + resp_h = resp.to_h + expect(resp_h['call']).not_to be_nil + # task_id should be nil for soft delete + expect(resp_h['task_id']).to be_nil + + # Verify not found (with retry for eventual consistency) + found = false + 5.times do + + sleep(1) + begin + get_call('default', call_id) + rescue GetStreamRuby::APIError => e + found = true if e.message.include?("Can't find call with id") + break + end + + end + expect(found).to be(true), 'Expected call to be not found after soft delete' + + end + + end + + # --------------------------------------------------------------------------- + # HardDeleteCall + # --------------------------------------------------------------------------- + + describe 'HardDeleteCall' do + + it 'hard deletes a call with task polling' do + + call_id = new_call_id + # Don't add to @created_call_ids since we're deleting it here + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + resp = delete_call_req('default', call_id, { hard: true }) + resp_h = resp.to_h + task_id = resp_h['task_id'] + expect(task_id).not_to be_nil + + task_result = wait_for_task(task_id) + expect(task_result.status).to eq('completed') + + # Verify not found (with retry for eventual consistency) + found = false + 5.times do + + sleep(1) + begin + get_call('default', call_id) + rescue GetStreamRuby::APIError => e + found = true if e.message.include?("Can't find call with id") + break + end + + end + expect(found).to be(true), 'Expected call to be not found after hard delete' + + end + + end + + # --------------------------------------------------------------------------- + # Teams + # --------------------------------------------------------------------------- + + describe 'Teams' do + + it 'creates a user with teams, creates a call with team, queries' do + + team_user_id = "test-user-#{SecureRandom.uuid}" + @created_user_ids << team_user_id + + @client.common.update_users( + GetStream::Generated::Models::UpdateUsersRequest.new( + users: { + team_user_id => GetStream::Generated::Models::UserRequest.new( + id: team_user_id, + name: 'Team User', + role: 'user', + teams: %w[red blue], + ), + }, + ), + ) + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + resp = create_call('default', call_id, { + data: { + created_by_id: team_user_id, + team: 'blue', + }, + }) + call_h = resp.to_h + expect(call_h.dig('call', 'team')).to eq('blue') + + # Query calls by team + query_resp = @client.make_request(:post, '/api/v2/video/calls', body: { + filter_conditions: { + 'id' => call_id, + 'team' => { '$eq' => 'blue' }, + }, + }) + query_h = query_resp.to_h + expect(query_h['calls'].length).to be >= 1 + + end + + end + + # --------------------------------------------------------------------------- + # ExternalStorageOperations + # --------------------------------------------------------------------------- + + describe 'ExternalStorageOperations' do + + it 'creates, lists, and deletes external storage' do + + storage_name = "test-storage-#{random_string(10)}" + + # Create external storage (fake credentials for API contract testing only) + create_resp = @client.make_request(:post, '/api/v2/external_storage', body: { + bucket: 'test-bucket', + name: storage_name, + storage_type: 's3', + path: 'test-directory/', + 'aws_s3' => { + 's3_region' => 'us-east-1', + 's3_api_key' => 'test-access-key', + 's3_secret' => 'test-secret', + }, + }) + expect(create_resp).not_to be_nil + + # Verify via list (with retry for eventual consistency) + found = false + 10.times do + + sleep(1) + list_resp = @client.make_request(:get, '/api/v2/external_storage') + storages_h = list_resp.to_h['external_storages'] || {} + if storages_h.key?(storage_name) + found = true + break + end + + end + expect(found).to be(true), "Expected storage #{storage_name} to appear in list" + + # Delete external storage (with retry for eventual consistency) + 5.times do |i| + + @client.make_request(:delete, "/api/v2/external_storage/#{storage_name}") + break + rescue GetStreamRuby::APIError + raise if i == 4 + + sleep(2) + + end + + end + + end + + # --------------------------------------------------------------------------- + # EnableCallRecordingAndBackstageMode + # --------------------------------------------------------------------------- + + describe 'EnableCallRecordingAndBackstageMode' do + + it 'updates call settings for recording and backstage' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + # Enable recording + resp_1 = update_call('default', call_id, { + settings_override: { + recording: { mode: 'available', audio_only: true }, + }, + }) + call_h_1 = resp_1.to_h + expect(call_h_1.dig('call', 'settings', 'recording', 'mode')).to eq('available') + + # Enable backstage + resp_2 = update_call('default', call_id, { + settings_override: { + backstage: { enabled: true }, + }, + }) + call_h_2 = resp_2.to_h + expect(call_h_2.dig('call', 'settings', 'backstage', 'enabled')).to eq(true) + + end + + end + + # --------------------------------------------------------------------------- + # DeleteRecordingsAndTranscriptions + # --------------------------------------------------------------------------- + + describe 'DeleteRecordingsAndTranscriptions' do + + it 'returns error when deleting non-existent recording' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + url = "/api/v2/video/call/default/#{call_id}/non-existent-session/recordings/non-existent-filename" + expect { @client.make_request(:delete, url) }.to raise_error(GetStreamRuby::APIError) + + end + + it 'returns error when deleting non-existent transcription' do + + call_id = new_call_id + @created_call_ids << ['default', call_id] + + create_call('default', call_id, { + data: { created_by_id: @user_1 }, + }) + + url = "/api/v2/video/call/default/#{call_id}/non-existent-session/transcriptions/non-existent-filename" + expect { @client.make_request(:delete, url) }.to raise_error(GetStreamRuby::APIError) + + end + + end + +end diff --git a/test/webhook_test.rb b/test/webhook_test.rb index 229dc65..b0424fc 100644 --- a/test/webhook_test.rb +++ b/test/webhook_test.rb @@ -596,6 +596,11 @@ def test_parse_feeds_feed_group_deleted assert_equal 'StreamChat::FeedGroupDeletedEvent', event.class.name end + def test_parse_feeds_feed_group_restored + event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_group.restored"}') + assert_equal 'StreamChat::FeedGroupRestoredEvent', event.class.name + end + def test_parse_feeds_feed_member_added event = StreamChat::Webhook.parse_webhook_event('{"type":"feeds.feed_member.added"}') assert_equal 'StreamChat::FeedMemberAddedEvent', event.class.name @@ -851,6 +856,31 @@ def test_parse_user_updated assert_equal 'StreamChat::UserUpdatedEvent', event.class.name end + def test_parse_user_group_created + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.created"}') + assert_equal 'StreamChat::UserGroupCreatedEvent', event.class.name + end + + def test_parse_user_group_deleted + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.deleted"}') + assert_equal 'StreamChat::UserGroupDeletedEvent', event.class.name + end + + def test_parse_user_group_member_added + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_added"}') + assert_equal 'StreamChat::UserGroupMemberAddedEvent', event.class.name + end + + def test_parse_user_group_member_removed + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.member_removed"}') + assert_equal 'StreamChat::UserGroupMemberRemovedEvent', event.class.name + end + + def test_parse_user_group_updated + event = StreamChat::Webhook.parse_webhook_event('{"type":"user_group.updated"}') + assert_equal 'StreamChat::UserGroupUpdatedEvent', event.class.name + end + def test_parse_webhook_event_unknown_type assert_raises(ArgumentError) do StreamChat::Webhook.parse_webhook_event('{"type":"unknown.event"}')