diff --git a/app/models/blocked_tipline_user.rb b/app/models/blocked_tipline_user.rb new file mode 100644 index 0000000000..5eb56c7612 --- /dev/null +++ b/app/models/blocked_tipline_user.rb @@ -0,0 +1,2 @@ +class BlockedTiplineUser < ApplicationRecord +end diff --git a/app/models/bot/smooch.rb b/app/models/bot/smooch.rb index a2e50b2206..31fd749316 100644 --- a/app/models/bot/smooch.rb +++ b/app/models/bot/smooch.rb @@ -31,6 +31,7 @@ class CapiUnhandledMessageWarning < MessageDeliveryError; end include SmoochMenus include SmoochFields include SmoochLanguage + include SmoochBlocking ::ProjectMedia.class_eval do attr_accessor :smooch_message @@ -800,19 +801,6 @@ def self.add_hashtags(text, pm) end end - def self.ban_user(message) - unless message.nil? - uid = message['authorId'] - Rails.logger.info("[Smooch Bot] Banned user #{uid}") - Rails.cache.write("smooch:banned:#{uid}", message.to_json) - end - end - - def self.user_banned?(payload) - uid = payload.dig('appUser', '_id') - !uid.blank? && !Rails.cache.read("smooch:banned:#{uid}").nil? - end - # Don't save as a ProjectMedia if it contains only menu options def self.is_a_valid_text_message?(text) !text.split(/#{MESSAGE_BOUNDARY}|\s+/).reject{ |m| m =~ /^[0-9]*$/ }.empty? diff --git a/app/models/concerns/smooch_blocking.rb b/app/models/concerns/smooch_blocking.rb new file mode 100644 index 0000000000..cdf94b9e48 --- /dev/null +++ b/app/models/concerns/smooch_blocking.rb @@ -0,0 +1,46 @@ +require 'active_support/concern' + +module SmoochBlocking + extend ActiveSupport::Concern + + module ClassMethods + def ban_user(message) + unless message.nil? + uid = message['authorId'] + self.block_user(uid) + end + end + + def block_user_from_error_code(uid, error_code) + self.block_user(uid) if error_code == 131056 # Error of type "pair rate limit hit" + end + + def block_user(uid) + begin + block = BlockedTiplineUser.new(uid: uid) + block.skip_check_ability = true + block.save! + Rails.logger.info("[Smooch Bot] Blocked user #{uid}") + Rails.cache.write("smooch:banned:#{uid}", Time.now.to_i) + rescue ActiveRecord::RecordNotUnique + # User already blocked + Rails.logger.info("[Smooch Bot] User #{uid} already blocked") + end + end + + def unblock_user(uid) + BlockedTiplineUser.where(uid: uid).last.destroy! + Rails.logger.info("[Smooch Bot] Unblocked user #{uid}") + Rails.cache.delete("smooch:banned:#{uid}") + end + + def user_blocked?(uid) + !uid.blank? && (!Rails.cache.read("smooch:banned:#{uid}").nil? || BlockedTiplineUser.where(uid: uid).exists?) + end + + def user_banned?(payload) + uid = payload.dig('appUser', '_id') + self.user_blocked?(uid) + end + end +end diff --git a/app/models/concerns/smooch_capi.rb b/app/models/concerns/smooch_capi.rb index 4ad790ea7c..469ce56d51 100644 --- a/app/models/concerns/smooch_capi.rb +++ b/app/models/concerns/smooch_capi.rb @@ -281,13 +281,17 @@ def capi_send_message_to_user(uid, text, extra = {}, _force = false, preview_url response = http.request(req) if response.code.to_i >= 400 error_message = begin JSON.parse(response.body)['error']['message'] rescue response.body end + error_code = begin JSON.parse(response.body)['error']['code'] rescue nil end e = Bot::Smooch::CapiMessageDeliveryError.new(error_message) + self.block_user_from_error_code(uid, error_code) CheckSentry.notify(e, uid: uid, type: payload.dig(:type), template_name: payload.dig(:template, :name), template_language: payload.dig(:template, :language, :code), - error: response.body + error: response.body, + error_message: error_message, + error_code: error_code ) end response diff --git a/db/migrate/20231002202443_create_blocked_tipline_users.rb b/db/migrate/20231002202443_create_blocked_tipline_users.rb new file mode 100644 index 0000000000..3cc9c5bdaa --- /dev/null +++ b/db/migrate/20231002202443_create_blocked_tipline_users.rb @@ -0,0 +1,9 @@ +class CreateBlockedTiplineUsers < ActiveRecord::Migration[6.1] + def change + create_table :blocked_tipline_users do |t| + t.string :uid, null: false + t.timestamps + end + add_index :blocked_tipline_users, :uid, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index c8beea6b6d..b8982c24e3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_09_22_174044) do +ActiveRecord::Schema.define(version: 2023_10_02_202443) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -195,6 +195,13 @@ t.index ["user_id"], name: "index_assignments_on_user_id" end + create_table "blocked_tipline_users", force: :cascade do |t| + t.string "uid", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_blocked_tipline_users_on_uid", unique: true + end + create_table "bounces", id: :serial, force: :cascade do |t| t.string "email", null: false t.datetime "created_at", null: false diff --git a/lib/sample_data.rb b/lib/sample_data.rb index bd851ff9bf..7bacb897f1 100644 --- a/lib/sample_data.rb +++ b/lib/sample_data.rb @@ -1096,4 +1096,8 @@ def create_task_stuff(delete_existing = true) at = create_annotation_type annotation_type: 'task_response_free_text', label: 'Task Response Free Text' create_field_instance annotation_type_object: at, name: 'response_free_text', label: 'Response', field_type_object: text, optional: false end + + def create_blocked_tipline_user(options = {}) + BlockedTiplineUser.create!({ uid: random_string }.merge(options)) + end end diff --git a/test/models/blocked_tipline_user_test.rb b/test/models/blocked_tipline_user_test.rb new file mode 100644 index 0000000000..b6374f5555 --- /dev/null +++ b/test/models/blocked_tipline_user_test.rb @@ -0,0 +1,33 @@ +require_relative '../test_helper' + +class BlockedTiplineUserTest < ActiveSupport::TestCase + def setup + end + + def teardown + end + + test 'should create blocked tipline user' do + assert_difference 'BlockedTiplineUser.count' do + create_blocked_tipline_user + end + end + + test 'should not create blocked tipline user if UID is blank' do + assert_no_difference 'BlockedTiplineUser.count' do + assert_raises ActiveRecord::NotNullViolation do + create_blocked_tipline_user uid: nil + end + end + end + + test 'should not block the same user more than once' do + uid = random_string + create_blocked_tipline_user uid: uid + assert_no_difference 'BlockedTiplineUser.count' do + assert_raises ActiveRecord::RecordNotUnique do + create_blocked_tipline_user uid: uid + end + end + end +end diff --git a/test/models/concerns/smooch_capi_test.rb b/test/models/concerns/smooch_capi_test.rb index c73665c03f..791e18a7e0 100644 --- a/test/models/concerns/smooch_capi_test.rb +++ b/test/models/concerns/smooch_capi_test.rb @@ -286,4 +286,14 @@ def teardown WebMock.stub_request(:post, 'https://graph.facebook.com/v15.0/123456/messages').to_return(status: 200, body: { id: '123456' }.to_json) assert_equal 200, Bot::Smooch.send_message_to_user(@uid, 'Test', { 'type' => 'video', 'mediaUrl' => 'https://test.test/video.mp4' }).code.to_i end + + test 'should block user if WhatsApp reports pair rate limit hit' do + assert !Bot::Smooch.user_blocked?(@uid) + WebMock.stub_request(:post, 'https://graph.facebook.com/v15.0/123456/messages').to_return(status: 400, body: { error: { message: '(#131056) (Business Account, Consumer Account) pair rate limit hit', code: 131056 } }.to_json) + Bot::Smooch.send_message_to_user(@uid, 'Test', { 'type' => 'text', 'text' => 'Test' }) + Bot::Smooch.send_message_to_user(@uid, 'Test', { 'type' => 'text', 'text' => 'Test' }) # Race condition + assert Bot::Smooch.user_blocked?(@uid) + Bot::Smooch.unblock_user(@uid) + assert !Bot::Smooch.user_blocked?(@uid) + end end