From 6c1d51fd9a9d6a48b24423caf54da8dfd36e62ee Mon Sep 17 00:00:00 2001 From: Varsh Date: Thu, 20 Mar 2025 18:05:41 +0100 Subject: [PATCH 01/25] Add Moderation v2 API Support --- lib/stream-chat/client.rb | 224 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/lib/stream-chat/client.rb b/lib/stream-chat/client.rb index 4abb6fe..422f197 100644 --- a/lib/stream-chat/client.rb +++ b/lib/stream-chat/client.rb @@ -20,6 +20,226 @@ module StreamChat SOFT_DELETE = 'soft' HARD_DELETE = 'hard' + # ModerationV2 class provides all the endpoints related to moderation v2 + class ModerationV2 + extend T::Sig + + MODERATION_ENTITY_TYPES = { + user: 'stream:user', + message: 'stream:chat:v1:message' + }.freeze + + sig { params(client: Client).void } + def initialize(client) + @client = client + end + + # Flags a user with a reason + # + # @param [string] flagged_user_id User ID to be flagged + # @param [string] reason Reason for flagging the user + # @param [Hash] options Additional options for flagging the user + # @option options [String] :user_id User ID of the user who is flagging the target user + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(flagged_user_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag_user(flagged_user_id, reason, **options) + flag(MODERATION_ENTITY_TYPES[:user], flagged_user_id, '', reason, **options) + end + + # Flags a message with a reason + # + # @param [string] message_id Message ID to be flagged + # @param [string] reason Reason for flagging the message + # @param [Hash] options Additional options for flagging the message + # @option options [String] :user_id User ID of the user who is flagging the target message + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(message_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag_message(message_id, reason, **options) + flag(MODERATION_ENTITY_TYPES[:message], message_id, '', reason, **options) + end + + # Flags an entity with a reason + # + # @param [string] entity_type Entity type to be flagged + # @param [string] entity_id Entity ID to be flagged + # @param [string] entity_creator_id User ID of the entity creator + # @param [string] reason Reason for flagging the entity + # @param [Hash] options Additional options for flagging the entity + # @option options [String] :user_id User ID of the user who is flagging the target entity + # @option options [Hash] :moderation_payload Content to be flagged + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag(entity_type, entity_id, entity_creator_id, reason, **options) + @client.post('api/v2/moderation/flag', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + reason: reason, + **options + }) + end + + # Mutes a user + # + # @param [string] target_id User ID to be muted + # @param [Hash] options Additional options for muting the user + # @option options [String] :user_id User ID of the user who is muting the target user + # @option options [Integer] :timeout Timeout for the mute in minutes + sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def mute_user(target_id, **options) + @client.post('api/v2/moderation/mute', data: { + target_ids: [target_id], + **options + }) + end + + # Unmutes a user + # + # @param [string] target_id User ID to be unmuted + # @param [Hash] options Additional options for unmuting the user + # @option options [String] :user_id User ID of the user who is unmuting the target user + sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def unmute_user(target_id, **options) + @client.post('api/v2/moderation/unmute', data: { + target_ids: [target_id], + **options + }) + end + + # Gets moderation report for a user + # + # @param [string] user_id User ID for which moderation report is to be fetched + # @param [Hash] options Additional options for fetching the moderation report + # @option options [Boolean] :create_user_if_not_exists Create user if not exists + # @option options [Boolean] :include_user_blocks Include user blocks + # @option options [Boolean] :include_user_mutes Include user mutes + sig { params(user_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def get_user_moderation_report(user_id, **options) + @client.get('api/v2/moderation/user_report', params: { + user_id: user_id, + **options + }) + end + + # Queries review queue + # + # @param [Hash] filter_conditions Filter conditions for querying review queue + # @param [Array] sort Sort conditions for querying review queue + # @param [Hash] options Pagination options for querying review queue + sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } + def query_review_queue(filter_conditions = {}, sort = [], **options) + @client.post('api/v2/moderation/review_queue', data: { + filter: filter_conditions, + sort: StreamChat.get_sort_fields(sort), + **options + }) + end + + # Upserts moderation config + # + # @param [Hash] config Moderation config to be upserted + sig { params(config: T.untyped).returns(StreamChat::StreamResponse) } + def upsert_config(config) + @client.post('api/v2/moderation/config', data: config) + end + + # Gets moderation config + # + # @param [string] key Key for which moderation config is to be fetched + # @param [Hash] data Additional data + # @option data [String] :team Team name + sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } + def get_config(key, data = {}) + @client.get("api/v2/moderation/config/#{key}", params: data) + end + + # Deletes moderation config + # + # @param [string] key Key for which moderation config is to be deleted + # @param [Hash] data Additional data + # @option data [String] :team Team name + sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } + def delete_config(key, data = {}) + @client.delete("api/v2/moderation/config/#{key}", params: data) + end + + # Queries moderation configs + # + # @param [Hash] filter_conditions Filter conditions for querying moderation configs + # @param [Array] sort Sort conditions for querying moderation configs + # @param [Hash] options Additional options for querying moderation configs + sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } + def query_configs(filter_conditions, sort, **options) + @client.post('api/v2/moderation/configs', data: { + filter: filter_conditions, + sort: sort, + **options + }) + end + + # Submits a moderation action + # + # @param [string] action_type Type of action to submit + # @param [string] item_id ID of the item to submit action for + # @param [Hash] options Additional options for submitting the action + sig { params(action_type: String, item_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def submit_action(action_type, item_id, **options) + @client.post('api/v2/moderation/submit_action', data: { + action_type: action_type, + item_id: item_id, + **options + }) + end + + # Checks content for moderation + # + # @param [string] entity_type Type of entity to be checked + # @param [string] entity_id ID of the entity to be checked + # @param [string] entity_creator_id ID of the entity creator + # @param [Hash] moderation_payload Content to be checked for moderation + # @param [string] config_key Key of the moderation config to use + # @param [Hash] options Additional options + # @option options [Boolean] :force_sync Force synchronous check + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_key, **options) + @client.post('api/v2/moderation/check', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + config_key: config_key, + options: options + }) + end + + # Adds custom flags to an entity + # + # @param [string] entity_type Type of entity to be checked + # @param [string] entity_id ID of the entity to be checked + # @param [string] entity_creator_id ID of the entity creator + # @param [Hash] moderation_payload Content to be checked for moderation + # @param [Array] flags Array of custom flags to add + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } + def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) + @client.post('api/v2/moderation/custom_check', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + flags: flags + }) + end + + # Adds custom flags to a message + # + # @param [string] message_id Message ID to be flagged + # @param [Array] flags Array of custom flags to add + sig { params(message_id: String, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } + def add_custom_message_flags(message_id, flags) + add_custom_flags(MODERATION_ENTITY_TYPES[:message], message_id, '', {}, flags) + end + end + class Client extend T::Sig @@ -35,6 +255,9 @@ class Client sig { returns(Faraday::Connection) } attr_reader :conn + sig { returns(ModerationV2) } + attr_reader :moderation + # initializes a Stream Chat API Client # # @param [string] api_key your application api_key @@ -64,6 +287,7 @@ def initialize(api_key, api_secret, timeout = nil, **options) end end @conn = T.let(conn, Faraday::Connection) + @moderation = T.let(ModerationV2.new(self), ModerationV2) end # initializes a Stream Chat API Client from STREAM_KEY and STREAM_SECRET From 67d16ede71987843444a867f36185afdf6b4d3e0 Mon Sep 17 00:00:00 2001 From: Varsh Date: Thu, 20 Mar 2025 18:13:23 +0100 Subject: [PATCH 02/25] fix rubocop errors --- lib/stream-chat/client.rb | 80 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/stream-chat/client.rb b/lib/stream-chat/client.rb index 422f197..25a891c 100644 --- a/lib/stream-chat/client.rb +++ b/lib/stream-chat/client.rb @@ -71,12 +71,12 @@ def flag_message(message_id, reason, **options) sig { params(entity_type: String, entity_id: String, entity_creator_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } def flag(entity_type, entity_id, entity_creator_id, reason, **options) @client.post('api/v2/moderation/flag', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - reason: reason, - **options - }) + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + reason: reason, + **options + }) end # Mutes a user @@ -88,9 +88,9 @@ def flag(entity_type, entity_id, entity_creator_id, reason, **options) sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } def mute_user(target_id, **options) @client.post('api/v2/moderation/mute', data: { - target_ids: [target_id], - **options - }) + target_ids: [target_id], + **options + }) end # Unmutes a user @@ -101,9 +101,9 @@ def mute_user(target_id, **options) sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } def unmute_user(target_id, **options) @client.post('api/v2/moderation/unmute', data: { - target_ids: [target_id], - **options - }) + target_ids: [target_id], + **options + }) end # Gets moderation report for a user @@ -116,9 +116,9 @@ def unmute_user(target_id, **options) sig { params(user_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } def get_user_moderation_report(user_id, **options) @client.get('api/v2/moderation/user_report', params: { - user_id: user_id, - **options - }) + user_id: user_id, + **options + }) end # Queries review queue @@ -129,10 +129,10 @@ def get_user_moderation_report(user_id, **options) sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } def query_review_queue(filter_conditions = {}, sort = [], **options) @client.post('api/v2/moderation/review_queue', data: { - filter: filter_conditions, - sort: StreamChat.get_sort_fields(sort), - **options - }) + filter: filter_conditions, + sort: StreamChat.get_sort_fields(sort), + **options + }) end # Upserts moderation config @@ -171,10 +171,10 @@ def delete_config(key, data = {}) sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } def query_configs(filter_conditions, sort, **options) @client.post('api/v2/moderation/configs', data: { - filter: filter_conditions, - sort: sort, - **options - }) + filter: filter_conditions, + sort: sort, + **options + }) end # Submits a moderation action @@ -185,10 +185,10 @@ def query_configs(filter_conditions, sort, **options) sig { params(action_type: String, item_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } def submit_action(action_type, item_id, **options) @client.post('api/v2/moderation/submit_action', data: { - action_type: action_type, - item_id: item_id, - **options - }) + action_type: action_type, + item_id: item_id, + **options + }) end # Checks content for moderation @@ -203,13 +203,13 @@ def submit_action(action_type, item_id, **options) sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_key, **options) @client.post('api/v2/moderation/check', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - moderation_payload: moderation_payload, - config_key: config_key, - options: options - }) + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + config_key: config_key, + options: options + }) end # Adds custom flags to an entity @@ -222,12 +222,12 @@ def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_ sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) @client.post('api/v2/moderation/custom_check', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - moderation_payload: moderation_payload, - flags: flags - }) + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + flags: flags + }) end # Adds custom flags to a message From 449faebad0f3cb7fea84c73fc731432e5aa27160 Mon Sep 17 00:00:00 2001 From: Varsh Date: Thu, 20 Mar 2025 18:16:48 +0100 Subject: [PATCH 03/25] fix rubocop errors --- lib/stream-chat/client.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/stream-chat/client.rb b/lib/stream-chat/client.rb index 25a891c..70a9c21 100644 --- a/lib/stream-chat/client.rb +++ b/lib/stream-chat/client.rb @@ -201,7 +201,14 @@ def submit_action(action_type, item_id, **options) # @param [Hash] options Additional options # @option options [Boolean] :force_sync Force synchronous check sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_key, **options) + def check(params = {}) + entity_type = params[:entity_type] + entity_id = params[:entity_id] + entity_creator_id = params[:entity_creator_id] + moderation_payload = params[:moderation_payload] + config_key = params[:config_key] + options = params[:options] || {} + @client.post('api/v2/moderation/check', data: { entity_type: entity_type, entity_id: entity_id, From 48140f83bff530b0e963e2bc783a6623b84d9dcf Mon Sep 17 00:00:00 2001 From: Varsh Date: Fri, 21 Mar 2025 11:58:35 +0100 Subject: [PATCH 04/25] seperate moderation into different file --- lib/stream-chat/client.rb | 232 +-------------------------------- lib/stream-chat/moderation.rb | 236 ++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 229 deletions(-) create mode 100644 lib/stream-chat/moderation.rb diff --git a/lib/stream-chat/client.rb b/lib/stream-chat/client.rb index 70a9c21..fce164d 100644 --- a/lib/stream-chat/client.rb +++ b/lib/stream-chat/client.rb @@ -14,239 +14,13 @@ require 'stream-chat/version' require 'stream-chat/util' require 'stream-chat/types' +require 'stream-chat/moderation' module StreamChat DEFAULT_BLOCKLIST = 'profanity_en_2020_v1' SOFT_DELETE = 'soft' HARD_DELETE = 'hard' - # ModerationV2 class provides all the endpoints related to moderation v2 - class ModerationV2 - extend T::Sig - - MODERATION_ENTITY_TYPES = { - user: 'stream:user', - message: 'stream:chat:v1:message' - }.freeze - - sig { params(client: Client).void } - def initialize(client) - @client = client - end - - # Flags a user with a reason - # - # @param [string] flagged_user_id User ID to be flagged - # @param [string] reason Reason for flagging the user - # @param [Hash] options Additional options for flagging the user - # @option options [String] :user_id User ID of the user who is flagging the target user - # @option options [Hash] :custom Additional data to be stored with the flag - sig { params(flagged_user_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def flag_user(flagged_user_id, reason, **options) - flag(MODERATION_ENTITY_TYPES[:user], flagged_user_id, '', reason, **options) - end - - # Flags a message with a reason - # - # @param [string] message_id Message ID to be flagged - # @param [string] reason Reason for flagging the message - # @param [Hash] options Additional options for flagging the message - # @option options [String] :user_id User ID of the user who is flagging the target message - # @option options [Hash] :custom Additional data to be stored with the flag - sig { params(message_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def flag_message(message_id, reason, **options) - flag(MODERATION_ENTITY_TYPES[:message], message_id, '', reason, **options) - end - - # Flags an entity with a reason - # - # @param [string] entity_type Entity type to be flagged - # @param [string] entity_id Entity ID to be flagged - # @param [string] entity_creator_id User ID of the entity creator - # @param [string] reason Reason for flagging the entity - # @param [Hash] options Additional options for flagging the entity - # @option options [String] :user_id User ID of the user who is flagging the target entity - # @option options [Hash] :moderation_payload Content to be flagged - # @option options [Hash] :custom Additional data to be stored with the flag - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def flag(entity_type, entity_id, entity_creator_id, reason, **options) - @client.post('api/v2/moderation/flag', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - reason: reason, - **options - }) - end - - # Mutes a user - # - # @param [string] target_id User ID to be muted - # @param [Hash] options Additional options for muting the user - # @option options [String] :user_id User ID of the user who is muting the target user - # @option options [Integer] :timeout Timeout for the mute in minutes - sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def mute_user(target_id, **options) - @client.post('api/v2/moderation/mute', data: { - target_ids: [target_id], - **options - }) - end - - # Unmutes a user - # - # @param [string] target_id User ID to be unmuted - # @param [Hash] options Additional options for unmuting the user - # @option options [String] :user_id User ID of the user who is unmuting the target user - sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def unmute_user(target_id, **options) - @client.post('api/v2/moderation/unmute', data: { - target_ids: [target_id], - **options - }) - end - - # Gets moderation report for a user - # - # @param [string] user_id User ID for which moderation report is to be fetched - # @param [Hash] options Additional options for fetching the moderation report - # @option options [Boolean] :create_user_if_not_exists Create user if not exists - # @option options [Boolean] :include_user_blocks Include user blocks - # @option options [Boolean] :include_user_mutes Include user mutes - sig { params(user_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def get_user_moderation_report(user_id, **options) - @client.get('api/v2/moderation/user_report', params: { - user_id: user_id, - **options - }) - end - - # Queries review queue - # - # @param [Hash] filter_conditions Filter conditions for querying review queue - # @param [Array] sort Sort conditions for querying review queue - # @param [Hash] options Pagination options for querying review queue - sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } - def query_review_queue(filter_conditions = {}, sort = [], **options) - @client.post('api/v2/moderation/review_queue', data: { - filter: filter_conditions, - sort: StreamChat.get_sort_fields(sort), - **options - }) - end - - # Upserts moderation config - # - # @param [Hash] config Moderation config to be upserted - sig { params(config: T.untyped).returns(StreamChat::StreamResponse) } - def upsert_config(config) - @client.post('api/v2/moderation/config', data: config) - end - - # Gets moderation config - # - # @param [string] key Key for which moderation config is to be fetched - # @param [Hash] data Additional data - # @option data [String] :team Team name - sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } - def get_config(key, data = {}) - @client.get("api/v2/moderation/config/#{key}", params: data) - end - - # Deletes moderation config - # - # @param [string] key Key for which moderation config is to be deleted - # @param [Hash] data Additional data - # @option data [String] :team Team name - sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } - def delete_config(key, data = {}) - @client.delete("api/v2/moderation/config/#{key}", params: data) - end - - # Queries moderation configs - # - # @param [Hash] filter_conditions Filter conditions for querying moderation configs - # @param [Array] sort Sort conditions for querying moderation configs - # @param [Hash] options Additional options for querying moderation configs - sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } - def query_configs(filter_conditions, sort, **options) - @client.post('api/v2/moderation/configs', data: { - filter: filter_conditions, - sort: sort, - **options - }) - end - - # Submits a moderation action - # - # @param [string] action_type Type of action to submit - # @param [string] item_id ID of the item to submit action for - # @param [Hash] options Additional options for submitting the action - sig { params(action_type: String, item_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def submit_action(action_type, item_id, **options) - @client.post('api/v2/moderation/submit_action', data: { - action_type: action_type, - item_id: item_id, - **options - }) - end - - # Checks content for moderation - # - # @param [string] entity_type Type of entity to be checked - # @param [string] entity_id ID of the entity to be checked - # @param [string] entity_creator_id ID of the entity creator - # @param [Hash] moderation_payload Content to be checked for moderation - # @param [string] config_key Key of the moderation config to use - # @param [Hash] options Additional options - # @option options [Boolean] :force_sync Force synchronous check - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def check(params = {}) - entity_type = params[:entity_type] - entity_id = params[:entity_id] - entity_creator_id = params[:entity_creator_id] - moderation_payload = params[:moderation_payload] - config_key = params[:config_key] - options = params[:options] || {} - - @client.post('api/v2/moderation/check', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - moderation_payload: moderation_payload, - config_key: config_key, - options: options - }) - end - - # Adds custom flags to an entity - # - # @param [string] entity_type Type of entity to be checked - # @param [string] entity_id ID of the entity to be checked - # @param [string] entity_creator_id ID of the entity creator - # @param [Hash] moderation_payload Content to be checked for moderation - # @param [Array] flags Array of custom flags to add - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } - def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) - @client.post('api/v2/moderation/custom_check', data: { - entity_type: entity_type, - entity_id: entity_id, - entity_creator_id: entity_creator_id, - moderation_payload: moderation_payload, - flags: flags - }) - end - - # Adds custom flags to a message - # - # @param [string] message_id Message ID to be flagged - # @param [Array] flags Array of custom flags to add - sig { params(message_id: String, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } - def add_custom_message_flags(message_id, flags) - add_custom_flags(MODERATION_ENTITY_TYPES[:message], message_id, '', {}, flags) - end - end - class Client extend T::Sig @@ -262,7 +36,7 @@ class Client sig { returns(Faraday::Connection) } attr_reader :conn - sig { returns(ModerationV2) } + sig { returns(Moderation) } attr_reader :moderation # initializes a Stream Chat API Client @@ -294,7 +68,7 @@ def initialize(api_key, api_secret, timeout = nil, **options) end end @conn = T.let(conn, Faraday::Connection) - @moderation = T.let(ModerationV2.new(self), ModerationV2) + @moderation = T.let(Moderation.new(self), Moderation) end # initializes a Stream Chat API Client from STREAM_KEY and STREAM_SECRET diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb new file mode 100644 index 0000000..b119ce3 --- /dev/null +++ b/lib/stream-chat/moderation.rb @@ -0,0 +1,236 @@ +# typed: strict +# frozen_string_literal: true + +require 'stream-chat/client' +require 'stream-chat/errors' +require 'stream-chat/util' +require 'stream-chat/types' + +module StreamChat + # Moderation class provides all the endpoints related to moderation v2 + class Moderation + extend T::Sig + + MODERATION_ENTITY_TYPES = { + user: 'stream:user', + message: 'stream:chat:v1:message' + }.freeze + + sig { params(client: Client).void } + def initialize(client) + @client = client + end + + # Flags a user with a reason + # + # @param [string] flagged_user_id User ID to be flagged + # @param [string] reason Reason for flagging the user + # @param [Hash] options Additional options for flagging the user + # @option options [String] :user_id User ID of the user who is flagging the target user + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(flagged_user_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag_user(flagged_user_id, reason, **options) + flag(MODERATION_ENTITY_TYPES[:user], flagged_user_id, '', reason, **options) + end + + # Flags a message with a reason + # + # @param [string] message_id Message ID to be flagged + # @param [string] reason Reason for flagging the message + # @param [Hash] options Additional options for flagging the message + # @option options [String] :user_id User ID of the user who is flagging the target message + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(message_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag_message(message_id, reason, **options) + flag(MODERATION_ENTITY_TYPES[:message], message_id, '', reason, **options) + end + + # Flags an entity with a reason + # + # @param [string] entity_type Entity type to be flagged + # @param [string] entity_id Entity ID to be flagged + # @param [string] entity_creator_id User ID of the entity creator + # @param [string] reason Reason for flagging the entity + # @param [Hash] options Additional options for flagging the entity + # @option options [String] :user_id User ID of the user who is flagging the target entity + # @option options [Hash] :moderation_payload Content to be flagged + # @option options [Hash] :custom Additional data to be stored with the flag + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag(entity_type, entity_id, entity_creator_id, reason, **options) + @client.post('api/v2/moderation/flag', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + reason: reason, + **options + }) + end + + # Mutes a user + # + # @param [string] target_id User ID to be muted + # @param [Hash] options Additional options for muting the user + # @option options [String] :user_id User ID of the user who is muting the target user + # @option options [Integer] :timeout Timeout for the mute in minutes + sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def mute_user(target_id, **options) + @client.post('api/v2/moderation/mute', data: { + target_ids: [target_id], + **options + }) + end + + # Unmutes a user + # + # @param [string] target_id User ID to be unmuted + # @param [Hash] options Additional options for unmuting the user + # @option options [String] :user_id User ID of the user who is unmuting the target user + sig { params(target_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def unmute_user(target_id, **options) + @client.post('api/v2/moderation/unmute', data: { + target_ids: [target_id], + **options + }) + end + + # Gets moderation report for a user + # + # @param [string] user_id User ID for which moderation report is to be fetched + # @param [Hash] options Additional options for fetching the moderation report + # @option options [Boolean] :create_user_if_not_exists Create user if not exists + # @option options [Boolean] :include_user_blocks Include user blocks + # @option options [Boolean] :include_user_mutes Include user mutes + sig { params(user_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def get_user_moderation_report(user_id, **options) + @client.get('api/v2/moderation/user_report', params: { + user_id: user_id, + **options + }) + end + + # Queries review queue + # + # @param [Hash] filter_conditions Filter conditions for querying review queue + # @param [Array] sort Sort conditions for querying review queue + # @param [Hash] options Pagination options for querying review queue + sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } + def query_review_queue(filter_conditions = {}, sort = [], **options) + @client.post('api/v2/moderation/review_queue', data: { + filter: filter_conditions, + sort: StreamChat.get_sort_fields(sort), + **options + }) + end + + # Upserts moderation config + # + # @param [Hash] config Moderation config to be upserted + sig { params(config: T.untyped).returns(StreamChat::StreamResponse) } + def upsert_config(config) + @client.post('api/v2/moderation/config', data: config) + end + + # Gets moderation config + # + # @param [string] key Key for which moderation config is to be fetched + # @param [Hash] data Additional data + # @option data [String] :team Team name + sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } + def get_config(key, data = {}) + @client.get("api/v2/moderation/config/#{key}", params: data) + end + + # Deletes moderation config + # + # @param [string] key Key for which moderation config is to be deleted + # @param [Hash] data Additional data + # @option data [String] :team Team name + sig { params(key: String, data: T.untyped).returns(StreamChat::StreamResponse) } + def delete_config(key, data = {}) + @client.delete("api/v2/moderation/config/#{key}", params: data) + end + + # Queries moderation configs + # + # @param [Hash] filter_conditions Filter conditions for querying moderation configs + # @param [Array] sort Sort conditions for querying moderation configs + # @param [Hash] options Additional options for querying moderation configs + sig { params(filter_conditions: T.untyped, sort: T.untyped, options: T.untyped).returns(StreamChat::StreamResponse) } + def query_configs(filter_conditions, sort, **options) + @client.post('api/v2/moderation/configs', data: { + filter: filter_conditions, + sort: sort, + **options + }) + end + + # Submits a moderation action + # + # @param [string] action_type Type of action to submit + # @param [string] item_id ID of the item to submit action for + # @param [Hash] options Additional options for submitting the action + sig { params(action_type: String, item_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def submit_action(action_type, item_id, **options) + @client.post('api/v2/moderation/submit_action', data: { + action_type: action_type, + item_id: item_id, + **options + }) + end + + # Checks content for moderation + # + # @param [string] entity_type Type of entity to be checked + # @param [string] entity_id ID of the entity to be checked + # @param [string] entity_creator_id ID of the entity creator + # @param [Hash] moderation_payload Content to be checked for moderation + # @param [string] config_key Key of the moderation config to use + # @param [Hash] options Additional options + # @option options [Boolean] :force_sync Force synchronous check + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def check(params = {}) + entity_type = params[:entity_type] + entity_id = params[:entity_id] + entity_creator_id = params[:entity_creator_id] + moderation_payload = params[:moderation_payload] + config_key = params[:config_key] + options = params[:options] || {} + + @client.post('api/v2/moderation/check', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + config_key: config_key, + options: options + }) + end + + # Adds custom flags to an entity + # + # @param [string] entity_type Type of entity to be checked + # @param [string] entity_id ID of the entity to be checked + # @param [string] entity_creator_id ID of the entity creator + # @param [Hash] moderation_payload Content to be checked for moderation + # @param [Array] flags Array of custom flags to add + sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } + def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) + @client.post('api/v2/moderation/custom_check', data: { + entity_type: entity_type, + entity_id: entity_id, + entity_creator_id: entity_creator_id, + moderation_payload: moderation_payload, + flags: flags + }) + end + + # Adds custom flags to a message + # + # @param [string] message_id Message ID to be flagged + # @param [Array] flags Array of custom flags to add + sig { params(message_id: String, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } + def add_custom_message_flags(message_id, flags) + add_custom_flags(MODERATION_ENTITY_TYPES[:message], message_id, '', {}, flags) + end + end +end From 614b0ed3c23ed98ac89b0d94c63b5114eba5df92 Mon Sep 17 00:00:00 2001 From: Varsh Date: Fri, 21 Mar 2025 15:43:59 +0100 Subject: [PATCH 05/25] add tests --- lib/stream-chat/moderation.rb | 31 +++++---- spec/client_spec.rb | 125 ++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index b119ce3..80e88ef 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -178,24 +178,31 @@ def submit_action(action_type, item_id, **options) }) end + # rubocop:disable Metrics/ParameterLists # Checks content for moderation # - # @param [string] entity_type Type of entity to be checked - # @param [string] entity_id ID of the entity to be checked + # @param [string] entity_type Type of entity to be checked E.g., stream:user, stream:chat:v1:message, or any custom string + # @param [string] entity_id ID of the entity to be checked. This is mainly for tracking purposes # @param [string] entity_creator_id ID of the entity creator # @param [Hash] moderation_payload Content to be checked for moderation + # @option moderation_payload [Array] :texts Array of texts to be checked for moderation + # @option moderation_payload [Array] :images Array of images to be checked for moderation + # @option moderation_payload [Array] :videos Array of videos to be checked for moderation + # @option moderation_payload [Hash] :custom Additional custom data # @param [string] config_key Key of the moderation config to use # @param [Hash] options Additional options # @option options [Boolean] :force_sync Force synchronous check - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, config_key: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def check(params = {}) - entity_type = params[:entity_type] - entity_id = params[:entity_id] - entity_creator_id = params[:entity_creator_id] - moderation_payload = params[:moderation_payload] - config_key = params[:config_key] - options = params[:options] || {} - + sig do + params( + entity_type: String, + entity_id: String, + entity_creator_id: String, + moderation_payload: T::Hash[Symbol, T.any(T::Array[String], T::Hash[String, T.untyped])], + config_key: String, + options: T::Hash[Symbol, T::Boolean] + ).returns(StreamChat::StreamResponse) + end + def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_key, options = {}) @client.post('api/v2/moderation/check', data: { entity_type: entity_type, entity_id: entity_id, @@ -205,7 +212,7 @@ def check(params = {}) options: options }) end - + # rubocop:enable Metrics/ParameterLists # Adds custom flags to an entity # # @param [string] entity_type Type of entity to be checked diff --git a/spec/client_spec.rb b/spec/client_spec.rb index cd4f87b..0c7a8ed 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -872,4 +872,129 @@ def loop_times(times) end end end + + describe 'moderation' do + before(:each) do + @moderation = @client.moderation + @test_user_id = SecureRandom.uuid + @test_message_id = SecureRandom.uuid + @test_config_key = SecureRandom.uuid + end + + it 'flagging a user and message' do + msg_response = @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) + expect(msg_response['message']['id']).to eq(@test_message_id) + expect(msg_response['message']['user']['id']).to eq(@test_user_id) + response = @moderation.flag_user( + @test_user_id, + 'inappropriate_behavior', + user_id: @random_user[:id], + custom: { severity: 'high' } + ) + expect(response['duration']).not_to be_nil + response = @moderation.flag_message( + @test_message_id, + 'inappropriate_content', + user_id: @random_user[:id], + custom: { category: 'spam' } + ) + expect(response['duration']).not_to be_nil + end + + it 'mute a user and unmute a user' do + @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) + testuserid1 = @random_user[:id] + response = @moderation.mute_user( + @test_user_id, + user_id: testuserid1, + timeout: 60 + ) + expect(response['duration']).not_to be_nil + expect(response['mutes'][0]['user']['id']).to eq(testuserid1) + response = @moderation.unmute_user( + @test_user_id, + user_id: @random_user[:id] + ) + expect(response['duration']).not_to be_nil + + response = @moderation.get_user_moderation_report( + @test_user_id, + include_user_blocks: true, + include_user_mutes: true + ) + expect(response['duration']).not_to be_nil + end + + it 'adds custom flags to an entity' do + testuserid1 = @random_user[:id] + testmsgid1 = SecureRandom.uuid + @channel.send_message({ id: testmsgid1, text: 'Test message' }, testuserid1) + entity_type = 'stream:chat:v1:message' + entity_id = testmsgid1 + entity_creator_id = testuserid1 + moderation_payload = { + 'texts' => ['Test message'], + 'custom' => { 'original_message_type' => 'regular' } + } + flags = [{ type: 'custom_check_text', value: 'test_flag' }] + + response = @moderation.add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) + expect(response['duration']).not_to be_nil + response = @moderation.add_custom_message_flags( + testmsgid1, + [{ type: 'custom_check_text', value: 'test_flag' }] + ) + expect(response['duration']).not_to be_nil + end + + it 'config test' do + blocklist_name = "blocklist-#{SecureRandom.uuid}" + words = %w[pretty crazy] + + # Create blocklist + response = @client.create_blocklist(blocklist_name, words) + expect(response['duration']).not_to be_nil + + # Create moderation config + moderation_config = { + key: "chat:team:#{@channel.id}", + block_list_config: { + enabled: true, + rules: [ + { + name: response['blocklist']['name'], + action: 'flag' + } + ] + } + } + @moderation.upsert_config(moderation_config) + response = @moderation.get_config("chat:team:#{@channel.id}") + expect(response['config']['key']).to eq("chat:team:#{@channel.id}") + + response = @moderation.query_configs( + { key: "chat:team:#{@channel.id}" }, + [] + ) + expect(response).not_to be_nil + + # Send message that should be blocked + + response = @channel.send_message( + { text: 'crazy game ever' }, + @random_user[:id], + force_moderation: true + ) + + # # Verify message appears in review queue + queue_response = @moderation.query_review_queue( + { entity_type: 'stream:chat:v1:message' }, + { created_at: -1 }, + limit: 1 + ) + expect(queue_response['items'][0]['entity_id']).to eq(response['message']['id']) + + @moderation.delete_config("chat:team:#{@channel.id}") + end + end end From 4b383e1ceb10622d1965b543e82a49acbbea6849 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 11:42:00 +0100 Subject: [PATCH 06/25] Correct the test --- spec/client_spec.rb | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 0c7a8ed..463d3c1 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -948,45 +948,39 @@ def loop_times(times) end it 'config test' do - blocklist_name = "blocklist-#{SecureRandom.uuid}" - words = %w[pretty crazy] - - # Create blocklist - response = @client.create_blocklist(blocklist_name, words) - expect(response['duration']).not_to be_nil - + newchannel = @client.channel('messaging', channel_id: 'fellowship-of-the-throne', + data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) # Create moderation config moderation_config = { - key: "chat:team:#{@channel.id}", + key: "chat:messaging:#{newchannel.id}", block_list_config: { enabled: true, rules: [ { - name: response['blocklist']['name'], + name: 'profanity_en_2020_v1', action: 'flag' } ] } } @moderation.upsert_config(moderation_config) - response = @moderation.get_config("chat:team:#{@channel.id}") - expect(response['config']['key']).to eq("chat:team:#{@channel.id}") + response = @moderation.get_config("chat:messaging:#{newchannel.id}") + expect(response['config']['key']).to eq("chat:messaging:#{newchannel.id}") response = @moderation.query_configs( - { key: "chat:team:#{@channel.id}" }, + { key: "chat:messaging:#{newchannel.id}" }, [] ) expect(response).not_to be_nil # Send message that should be blocked - - response = @channel.send_message( - { text: 'crazy game ever' }, + response = newchannel.send_message( + { text: 'damn' }, @random_user[:id], force_moderation: true ) - # # Verify message appears in review queue + # Verify message appears in review queue queue_response = @moderation.query_review_queue( { entity_type: 'stream:chat:v1:message' }, { created_at: -1 }, @@ -994,7 +988,7 @@ def loop_times(times) ) expect(queue_response['items'][0]['entity_id']).to eq(response['message']['id']) - @moderation.delete_config("chat:team:#{@channel.id}") + @moderation.delete_config("chat:messaging:#{newchannel.id}") end end end From 437d3030800c6ab58e85c4d1d14e7c3c7eecb29e Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 11:46:27 +0100 Subject: [PATCH 07/25] Correct the tests --- spec/client_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 463d3c1..32e91b3 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -950,6 +950,7 @@ def loop_times(times) it 'config test' do newchannel = @client.channel('messaging', channel_id: 'fellowship-of-the-throne', data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) + newchannel.create('legolas') # Create moderation config moderation_config = { key: "chat:messaging:#{newchannel.id}", From 68175bbe16c7294ef559d7e760b0932fe14322d2 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 11:49:13 +0100 Subject: [PATCH 08/25] Correct the tests --- spec/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 32e91b3..aad7741 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -950,7 +950,7 @@ def loop_times(times) it 'config test' do newchannel = @client.channel('messaging', channel_id: 'fellowship-of-the-throne', data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) - newchannel.create('legolas') + newchannel.create('tav') # Create moderation config moderation_config = { key: "chat:messaging:#{newchannel.id}", From b499b9c0105009cc29374c190c29f90540e617e7 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 11:56:00 +0100 Subject: [PATCH 09/25] Simplify the test --- spec/client_spec.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index aad7741..ddc861f 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -948,12 +948,9 @@ def loop_times(times) end it 'config test' do - newchannel = @client.channel('messaging', channel_id: 'fellowship-of-the-throne', - data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) - newchannel.create('tav') # Create moderation config moderation_config = { - key: "chat:messaging:#{newchannel.id}", + key: "chat:team:#{@channel.id}", block_list_config: { enabled: true, rules: [ @@ -965,17 +962,17 @@ def loop_times(times) } } @moderation.upsert_config(moderation_config) - response = @moderation.get_config("chat:messaging:#{newchannel.id}") - expect(response['config']['key']).to eq("chat:messaging:#{newchannel.id}") + response = @moderation.get_config("chat:team:#{@channel.id}") + expect(response['config']['key']).to eq("chat:team:#{@channel.id}") response = @moderation.query_configs( - { key: "chat:messaging:#{newchannel.id}" }, + { key: "chat:messaging:#{@channel.id}" }, [] ) expect(response).not_to be_nil # Send message that should be blocked - response = newchannel.send_message( + response = @channel.send_message( { text: 'damn' }, @random_user[:id], force_moderation: true @@ -989,7 +986,8 @@ def loop_times(times) ) expect(queue_response['items'][0]['entity_id']).to eq(response['message']['id']) - @moderation.delete_config("chat:messaging:#{newchannel.id}") + response = @moderation.delete_config("chat:team:#{@channel.id}") + expect(response['duration']).not_to be_nil end end end From 48be5a24642858b697e546da2f5ebe3875339687 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:10:40 +0100 Subject: [PATCH 10/25] test change - 1 --- spec/client_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index ddc861f..bc875b9 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -382,6 +382,7 @@ def loop_times(times) it 'queries channels' do response = @client.query_channels({ 'members' => { '$in' => ['legolas'] } }, sort: { 'id' => 1 }) + puts response expect(response['channels'].length).to eq 1 expect(response['channels'][0]['channel']['id']).to eq 'fellowship-of-the-ring' expect(response['channels'][0]['members'].length).to eq 4 From 4472b8da052011e758ac40728a4a2db0baf1c006 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:16:43 +0100 Subject: [PATCH 11/25] test change - 2 --- spec/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index bc875b9..024f278 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -382,7 +382,7 @@ def loop_times(times) it 'queries channels' do response = @client.query_channels({ 'members' => { '$in' => ['legolas'] } }, sort: { 'id' => 1 }) - puts response + puts response['channels'][1] expect(response['channels'].length).to eq 1 expect(response['channels'][0]['channel']['id']).to eq 'fellowship-of-the-ring' expect(response['channels'][0]['members'].length).to eq 4 From af91a99cf8a16aad382d71d02641426dc71bdf9c Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:20:08 +0100 Subject: [PATCH 12/25] test change - 3 --- spec/client_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 024f278..7a4de1f 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -35,6 +35,8 @@ def loop_times(times) @channel = @client.channel('team', channel_id: 'fellowship-of-the-ring', data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) @channel.create('gandalf') + response = @client.delete_channels(['fellowship-of-the-throne'], hard_delete: true) + puts response end before(:each) do From 2a93928a03cf6eb221f24dada37064e6805960cf Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:22:15 +0100 Subject: [PATCH 13/25] test change - 4 --- spec/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 7a4de1f..31db88b 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -35,7 +35,7 @@ def loop_times(times) @channel = @client.channel('team', channel_id: 'fellowship-of-the-ring', data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) @channel.create('gandalf') - response = @client.delete_channels(['fellowship-of-the-throne'], hard_delete: true) + response = @client.delete_channels(['messaging:fellowship-of-the-throne'], hard_delete: true) puts response end From bc1cb7879be1696347f6d3c5244d5c4a40a788c1 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:24:51 +0100 Subject: [PATCH 14/25] update the tests --- spec/client_spec.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 31db88b..024f278 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -35,8 +35,6 @@ def loop_times(times) @channel = @client.channel('team', channel_id: 'fellowship-of-the-ring', data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) @channel.create('gandalf') - response = @client.delete_channels(['messaging:fellowship-of-the-throne'], hard_delete: true) - puts response end before(:each) do From 5d5232428edafcaba47eeb49d4391738ac0947da Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:30:09 +0100 Subject: [PATCH 15/25] fix sorbet error --- lib/stream-chat/moderation.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index 80e88ef..5fda572 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -11,10 +11,13 @@ module StreamChat class Moderation extend T::Sig - MODERATION_ENTITY_TYPES = { - user: 'stream:user', - message: 'stream:chat:v1:message' - }.freeze + MODERATION_ENTITY_TYPES = T.let( + { + user: 'stream:user', + message: 'stream:chat:v1:message' + }.freeze, + T::Hash[T.untyped, T.untyped] + ) sig { params(client: Client).void } def initialize(client) From a6ef04828513a2886bfc452fe1f77b311faa6f15 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 12:33:16 +0100 Subject: [PATCH 16/25] remove puts --- spec/client_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 024f278..ddc861f 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -382,7 +382,6 @@ def loop_times(times) it 'queries channels' do response = @client.query_channels({ 'members' => { '$in' => ['legolas'] } }, sort: { 'id' => 1 }) - puts response['channels'][1] expect(response['channels'].length).to eq 1 expect(response['channels'][0]['channel']['id']).to eq 'fellowship-of-the-ring' expect(response['channels'][0]['members'].length).to eq 4 From 43122a12eec52eb15dab6f82e27101d60bfd3f4e Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 14:25:37 +0100 Subject: [PATCH 17/25] Update lib/stream-chat/moderation.rb Co-authored-by: nijeesh-stream --- lib/stream-chat/moderation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index 5fda572..fbf49f0 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -16,7 +16,7 @@ class Moderation user: 'stream:user', message: 'stream:chat:v1:message' }.freeze, - T::Hash[T.untyped, T.untyped] + T::Hash[T.Symbol, T.String] ) sig { params(client: Client).void } From cd84f8bd0e74ec5a8308255ccdc25df40330ac8a Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 14:39:01 +0100 Subject: [PATCH 18/25] Correct types --- lib/stream-chat/moderation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index fbf49f0..aa80162 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -16,7 +16,7 @@ class Moderation user: 'stream:user', message: 'stream:chat:v1:message' }.freeze, - T::Hash[T.Symbol, T.String] + T::Hash[Symbol, String] ) sig { params(client: Client).void } From a82fd2168aa726dd86c8dbfd82418f6ad6afadb8 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 14:46:33 +0100 Subject: [PATCH 19/25] Correct types --- lib/stream-chat/moderation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index aa80162..02913d2 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -33,7 +33,7 @@ def initialize(client) # @option options [Hash] :custom Additional data to be stored with the flag sig { params(flagged_user_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } def flag_user(flagged_user_id, reason, **options) - flag(MODERATION_ENTITY_TYPES[:user], flagged_user_id, '', reason, **options) + flag(T.must(MODERATION_ENTITY_TYPES[:user]), flagged_user_id, '', reason, **options) end # Flags a message with a reason @@ -45,7 +45,7 @@ def flag_user(flagged_user_id, reason, **options) # @option options [Hash] :custom Additional data to be stored with the flag sig { params(message_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } def flag_message(message_id, reason, **options) - flag(MODERATION_ENTITY_TYPES[:message], message_id, '', reason, **options) + flag(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, '', reason, **options) end # Flags an entity with a reason From a5db63be10c579b051253646a04f8b755082d6bd Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 14:56:33 +0100 Subject: [PATCH 20/25] Correct types --- lib/stream-chat/moderation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index 02913d2..26027be 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -240,7 +240,7 @@ def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_paylo # @param [Array] flags Array of custom flags to add sig { params(message_id: String, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } def add_custom_message_flags(message_id, flags) - add_custom_flags(MODERATION_ENTITY_TYPES[:message], message_id, '', {}, flags) + add_custom_flags(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, '', {}, flags) end end end From 0c16fa7a7a1eb7940b76ec1e141e4695c3831fc0 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 15:05:44 +0100 Subject: [PATCH 21/25] Make entity_creator_id optional --- lib/stream-chat/moderation.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index 26027be..c622747 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -52,14 +52,14 @@ def flag_message(message_id, reason, **options) # # @param [string] entity_type Entity type to be flagged # @param [string] entity_id Entity ID to be flagged - # @param [string] entity_creator_id User ID of the entity creator # @param [string] reason Reason for flagging the entity + # @param [string] entity_creator_id User ID of the entity creator (optional) # @param [Hash] options Additional options for flagging the entity # @option options [String] :user_id User ID of the user who is flagging the target entity # @option options [Hash] :moderation_payload Content to be flagged # @option options [Hash] :custom Additional data to be stored with the flag - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } - def flag(entity_type, entity_id, entity_creator_id, reason, **options) + sig { params(entity_type: String, entity_id: String, reason: String, entity_creator_id: String, options: T.untyped).returns(StreamChat::StreamResponse) } + def flag(entity_type, entity_id, reason, entity_creator_id: '', **options) @client.post('api/v2/moderation/flag', data: { entity_type: entity_type, entity_id: entity_id, @@ -199,13 +199,13 @@ def submit_action(action_type, item_id, **options) params( entity_type: String, entity_id: String, - entity_creator_id: String, moderation_payload: T::Hash[Symbol, T.any(T::Array[String], T::Hash[String, T.untyped])], config_key: String, + entity_creator_id: String, options: T::Hash[Symbol, T::Boolean] ).returns(StreamChat::StreamResponse) end - def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_key, options = {}) + def check(entity_type, entity_id, moderation_payload, config_key, entity_creator_id: '', options: {}) @client.post('api/v2/moderation/check', data: { entity_type: entity_type, entity_id: entity_id, @@ -223,8 +223,8 @@ def check(entity_type, entity_id, entity_creator_id, moderation_payload, config_ # @param [string] entity_creator_id ID of the entity creator # @param [Hash] moderation_payload Content to be checked for moderation # @param [Array] flags Array of custom flags to add - sig { params(entity_type: String, entity_id: String, entity_creator_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } - def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) + sig { params(entity_type: String, entity_id: String, moderation_payload: T.untyped, flags: T::Array[T.untyped], entity_creator_id: String).returns(StreamChat::StreamResponse) } + def add_custom_flags(entity_type, entity_id, moderation_payload, flags, entity_creator_id: '') @client.post('api/v2/moderation/custom_check', data: { entity_type: entity_type, entity_id: entity_id, @@ -240,7 +240,7 @@ def add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_paylo # @param [Array] flags Array of custom flags to add sig { params(message_id: String, flags: T::Array[T.untyped]).returns(StreamChat::StreamResponse) } def add_custom_message_flags(message_id, flags) - add_custom_flags(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, '', {}, flags) + add_custom_flags(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, {}, flags) end end end From ea576862657d3d8df21e53d44fca1f08a5f5ebb8 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 15:15:57 +0100 Subject: [PATCH 22/25] Fix type signatures --- lib/stream-chat/moderation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stream-chat/moderation.rb b/lib/stream-chat/moderation.rb index c622747..1e77b44 100644 --- a/lib/stream-chat/moderation.rb +++ b/lib/stream-chat/moderation.rb @@ -33,7 +33,7 @@ def initialize(client) # @option options [Hash] :custom Additional data to be stored with the flag sig { params(flagged_user_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } def flag_user(flagged_user_id, reason, **options) - flag(T.must(MODERATION_ENTITY_TYPES[:user]), flagged_user_id, '', reason, **options) + flag(T.must(MODERATION_ENTITY_TYPES[:user]), flagged_user_id, reason, **options) end # Flags a message with a reason @@ -45,7 +45,7 @@ def flag_user(flagged_user_id, reason, **options) # @option options [Hash] :custom Additional data to be stored with the flag sig { params(message_id: String, reason: String, options: T.untyped).returns(StreamChat::StreamResponse) } def flag_message(message_id, reason, **options) - flag(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, '', reason, **options) + flag(T.must(MODERATION_ENTITY_TYPES[:message]), message_id, reason, **options) end # Flags an entity with a reason From d74fb6d569bff18af90f8afbd61cb422d6084b78 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 15:26:25 +0100 Subject: [PATCH 23/25] Fix type signatures --- spec/client_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index ddc861f..c5dfc15 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -931,14 +931,13 @@ def loop_times(times) @channel.send_message({ id: testmsgid1, text: 'Test message' }, testuserid1) entity_type = 'stream:chat:v1:message' entity_id = testmsgid1 - entity_creator_id = testuserid1 moderation_payload = { 'texts' => ['Test message'], 'custom' => { 'original_message_type' => 'regular' } } flags = [{ type: 'custom_check_text', value: 'test_flag' }] - response = @moderation.add_custom_flags(entity_type, entity_id, entity_creator_id, moderation_payload, flags) + response = @moderation.add_custom_flags(entity_type, entity_id,moderation_payload, flags, entity_creator_id: testuserid1) expect(response['duration']).not_to be_nil response = @moderation.add_custom_message_flags( testmsgid1, From cf8d70abae1ed7bf56aeebe1414b9534ae01a90d Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 15:27:59 +0100 Subject: [PATCH 24/25] Update spec/client_spec.rb Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- spec/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index c5dfc15..14769d5 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -937,7 +937,7 @@ def loop_times(times) } flags = [{ type: 'custom_check_text', value: 'test_flag' }] - response = @moderation.add_custom_flags(entity_type, entity_id,moderation_payload, flags, entity_creator_id: testuserid1) + response = @moderation.add_custom_flags(entity_type, entity_id, moderation_payload, flags, entity_creator_id: testuserid1) expect(response['duration']).not_to be_nil response = @moderation.add_custom_message_flags( testmsgid1, From acd3e10a0287d00bc584198a0be9fe28bcca5e38 Mon Sep 17 00:00:00 2001 From: Varsh Date: Mon, 24 Mar 2025 15:34:36 +0100 Subject: [PATCH 25/25] Seperate moderation spec to a different file --- spec/client_spec.rb | 117 ----------------------- spec/moderation_spec.rb | 205 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 117 deletions(-) create mode 100644 spec/moderation_spec.rb diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 14769d5..cd4f87b 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -872,121 +872,4 @@ def loop_times(times) end end end - - describe 'moderation' do - before(:each) do - @moderation = @client.moderation - @test_user_id = SecureRandom.uuid - @test_message_id = SecureRandom.uuid - @test_config_key = SecureRandom.uuid - end - - it 'flagging a user and message' do - msg_response = @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) - expect(msg_response['message']['id']).to eq(@test_message_id) - expect(msg_response['message']['user']['id']).to eq(@test_user_id) - response = @moderation.flag_user( - @test_user_id, - 'inappropriate_behavior', - user_id: @random_user[:id], - custom: { severity: 'high' } - ) - expect(response['duration']).not_to be_nil - response = @moderation.flag_message( - @test_message_id, - 'inappropriate_content', - user_id: @random_user[:id], - custom: { category: 'spam' } - ) - expect(response['duration']).not_to be_nil - end - - it 'mute a user and unmute a user' do - @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) - testuserid1 = @random_user[:id] - response = @moderation.mute_user( - @test_user_id, - user_id: testuserid1, - timeout: 60 - ) - expect(response['duration']).not_to be_nil - expect(response['mutes'][0]['user']['id']).to eq(testuserid1) - response = @moderation.unmute_user( - @test_user_id, - user_id: @random_user[:id] - ) - expect(response['duration']).not_to be_nil - - response = @moderation.get_user_moderation_report( - @test_user_id, - include_user_blocks: true, - include_user_mutes: true - ) - expect(response['duration']).not_to be_nil - end - - it 'adds custom flags to an entity' do - testuserid1 = @random_user[:id] - testmsgid1 = SecureRandom.uuid - @channel.send_message({ id: testmsgid1, text: 'Test message' }, testuserid1) - entity_type = 'stream:chat:v1:message' - entity_id = testmsgid1 - moderation_payload = { - 'texts' => ['Test message'], - 'custom' => { 'original_message_type' => 'regular' } - } - flags = [{ type: 'custom_check_text', value: 'test_flag' }] - - response = @moderation.add_custom_flags(entity_type, entity_id, moderation_payload, flags, entity_creator_id: testuserid1) - expect(response['duration']).not_to be_nil - response = @moderation.add_custom_message_flags( - testmsgid1, - [{ type: 'custom_check_text', value: 'test_flag' }] - ) - expect(response['duration']).not_to be_nil - end - - it 'config test' do - # Create moderation config - moderation_config = { - key: "chat:team:#{@channel.id}", - block_list_config: { - enabled: true, - rules: [ - { - name: 'profanity_en_2020_v1', - action: 'flag' - } - ] - } - } - @moderation.upsert_config(moderation_config) - response = @moderation.get_config("chat:team:#{@channel.id}") - expect(response['config']['key']).to eq("chat:team:#{@channel.id}") - - response = @moderation.query_configs( - { key: "chat:messaging:#{@channel.id}" }, - [] - ) - expect(response).not_to be_nil - - # Send message that should be blocked - response = @channel.send_message( - { text: 'damn' }, - @random_user[:id], - force_moderation: true - ) - - # Verify message appears in review queue - queue_response = @moderation.query_review_queue( - { entity_type: 'stream:chat:v1:message' }, - { created_at: -1 }, - limit: 1 - ) - expect(queue_response['items'][0]['entity_id']).to eq(response['message']['id']) - - response = @moderation.delete_config("chat:team:#{@channel.id}") - expect(response['duration']).not_to be_nil - end - end end diff --git a/spec/moderation_spec.rb b/spec/moderation_spec.rb new file mode 100644 index 0000000..6b0bc74 --- /dev/null +++ b/spec/moderation_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require 'jwt' +require 'securerandom' +require 'stream-chat' +require 'faraday' + +describe StreamChat::Moderation do + def loop_times(times) + loop do + begin + yield() + return + rescue StandardError, RSpec::Expectations::ExpectationNotMetError + raise if times.zero? + end + + sleep(1) + times -= 1 + end + end + + before(:all) do + @client = StreamChat::Client.from_env + + @created_users = [] + + @fellowship_of_the_ring = [ + { id: 'frodo-baggins', name: 'Frodo Baggins', race: 'Hobbit', age: 50 }, + { id: 'sam-gamgee', name: 'Samwise Gamgee', race: 'Hobbit', age: 38 }, + { id: 'gandalf', name: 'Gandalf the Grey', race: 'Istari' }, + { id: 'legolas', name: 'Legolas', race: 'Elf', age: 500 } + ] + @client.upsert_users(@fellowship_of_the_ring) + @channel = @client.channel('team', channel_id: 'fellowship-of-the-ring', + data: { members: @fellowship_of_the_ring.map { |fellow| fellow[:id] } }) + @channel.create('gandalf') + end + + before(:each) do + @random_users = [{ id: SecureRandom.uuid }, { id: SecureRandom.uuid }] + @random_user = { id: SecureRandom.uuid } + users_to_insert = [@random_users[0], @random_users[1], @random_user] + + @created_users.push(*users_to_insert.map { |u| u[:id] }) + + @client.upsert_users(users_to_insert) + end + + after(:all) do + curr_idx = 0 + batch_size = 25 + + slice = @created_users.slice(0, batch_size) + + while !slice.nil? && !slice.empty? + @client.delete_users(slice, user: StreamChat::HARD_DELETE, messages: StreamChat::HARD_DELETE) + + curr_idx += batch_size + slice = @created_users.slice(curr_idx, batch_size) + end + end + + it 'properly sets up a new client' do + client = StreamChat::Client.from_env + + client.set_http_client(Faraday.new(url: 'https://getstream.io')) + expect { client.get_app_settings }.to raise_error(StreamChat::StreamAPIException) + + client.set_http_client(Faraday.new(url: 'https://chat.stream-io-api.com')) + response = client.get_app_settings + expect(response).to include 'app' + end + + it 'raises ArgumentError if no api_key is provided' do + expect { StreamChat::Client.new(nil, nil) }.to raise_error(TypeError) + end + + it 'properly handles stream response class' do + response = @client.get_app_settings + expect(response.rate_limit.limit).to be > 0 + expect(response.rate_limit.remaining).to be > 0 + expect(response.rate_limit.reset).to be_within(120).of Time.now.utc + expect(response.status_code).to be 200 + expect(response.to_json).not_to include 'rate_limit' + expect(response.to_json).not_to include 'status_code' + end + + describe 'moderation' do + before(:each) do + @moderation = @client.moderation + @test_user_id = SecureRandom.uuid + @test_message_id = SecureRandom.uuid + @test_config_key = SecureRandom.uuid + end + + it 'flagging a user and message' do + msg_response = @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) + expect(msg_response['message']['id']).to eq(@test_message_id) + expect(msg_response['message']['user']['id']).to eq(@test_user_id) + response = @moderation.flag_user( + @test_user_id, + 'inappropriate_behavior', + user_id: @random_user[:id], + custom: { severity: 'high' } + ) + expect(response['duration']).not_to be_nil + response = @moderation.flag_message( + @test_message_id, + 'inappropriate_content', + user_id: @random_user[:id], + custom: { category: 'spam' } + ) + expect(response['duration']).not_to be_nil + end + + it 'mute a user and unmute a user' do + @channel.send_message({ id: @test_message_id, text: 'Test message' }, @test_user_id) + testuserid1 = @random_user[:id] + response = @moderation.mute_user( + @test_user_id, + user_id: testuserid1, + timeout: 60 + ) + expect(response['duration']).not_to be_nil + expect(response['mutes'][0]['user']['id']).to eq(testuserid1) + response = @moderation.unmute_user( + @test_user_id, + user_id: @random_user[:id] + ) + expect(response['duration']).not_to be_nil + + response = @moderation.get_user_moderation_report( + @test_user_id, + include_user_blocks: true, + include_user_mutes: true + ) + expect(response['duration']).not_to be_nil + end + + it 'adds custom flags to an entity' do + testuserid1 = @random_user[:id] + testmsgid1 = SecureRandom.uuid + @channel.send_message({ id: testmsgid1, text: 'Test message' }, testuserid1) + entity_type = 'stream:chat:v1:message' + entity_id = testmsgid1 + moderation_payload = { + 'texts' => ['Test message'], + 'custom' => { 'original_message_type' => 'regular' } + } + flags = [{ type: 'custom_check_text', value: 'test_flag' }] + + response = @moderation.add_custom_flags(entity_type, entity_id, moderation_payload, flags, entity_creator_id: testuserid1) + expect(response['duration']).not_to be_nil + response = @moderation.add_custom_message_flags( + testmsgid1, + [{ type: 'custom_check_text', value: 'test_flag' }] + ) + expect(response['duration']).not_to be_nil + end + + it 'config test' do + # Create moderation config + moderation_config = { + key: "chat:team:#{@channel.id}", + block_list_config: { + enabled: true, + rules: [ + { + name: 'profanity_en_2020_v1', + action: 'flag' + } + ] + } + } + @moderation.upsert_config(moderation_config) + response = @moderation.get_config("chat:team:#{@channel.id}") + expect(response['config']['key']).to eq("chat:team:#{@channel.id}") + + response = @moderation.query_configs( + { key: "chat:messaging:#{@channel.id}" }, + [] + ) + expect(response).not_to be_nil + + # Send message that should be blocked + response = @channel.send_message( + { text: 'damn' }, + @random_user[:id], + force_moderation: true + ) + + # Verify message appears in review queue + queue_response = @moderation.query_review_queue( + { entity_type: 'stream:chat:v1:message' }, + { created_at: -1 }, + limit: 1 + ) + expect(queue_response['items'][0]['entity_id']).to eq(response['message']['id']) + + response = @moderation.delete_config("chat:team:#{@channel.id}") + expect(response['duration']).not_to be_nil + end + end +end