Skip to content

Commit

Permalink
Block WhatsApp user if WhatsApp API reports a "pair rate limit hit" e…
Browse files Browse the repository at this point in the history
…rror

If WhatsApp Cloud API reports an error of type "pair rate limit hit" when we try to send a message to a WhatsApp user, block that user automatically, so we don't need to answer them again.

It helps with SPAM and abuse.

Reference: CV2-3489.
  • Loading branch information
caiosba authored Oct 4, 2023
1 parent 68fdd42 commit 54fb377
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 15 deletions.
2 changes: 2 additions & 0 deletions app/models/blocked_tipline_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class BlockedTiplineUser < ApplicationRecord
end
14 changes: 1 addition & 13 deletions app/models/bot/smooch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CapiUnhandledMessageWarning < MessageDeliveryError; end
include SmoochMenus
include SmoochFields
include SmoochLanguage
include SmoochBlocking

::ProjectMedia.class_eval do
attr_accessor :smooch_message
Expand Down Expand Up @@ -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?
Expand Down
46 changes: 46 additions & 0 deletions app/models/concerns/smooch_blocking.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion app/models/concerns/smooch_capi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20231002202443_create_blocked_tipline_users.rb
Original file line number Diff line number Diff line change
@@ -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
9 changes: 8 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/sample_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 33 additions & 0 deletions test/models/blocked_tipline_user_test.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions test/models/concerns/smooch_capi_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 54fb377

Please sign in to comment.