Block same body over 5 times in 120s

Stephen Paul Weber created

Change summary

lib/customer_usage.rb | 21 ++++++++++++++++++---
sgx_jmp.rb            | 23 +++++++++++++++--------
2 files changed, 33 insertions(+), 11 deletions(-)

Detailed changes

lib/customer_usage.rb 🔗

@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require "digest"
+
 require_relative "./usage_report"
 
 class CustomerUsage
@@ -26,7 +28,7 @@ class CustomerUsage
 		)
 	end
 
-	def incr_message_usage(amount=1)
+	def incr_message_usage(amount=1, body=nil)
 		today = Time.now.utc.to_date
 		EMPromise.all([
 			expire_message_usage,
@@ -34,8 +36,21 @@ class CustomerUsage
 				"jmp_customer_outbound_messages-#{@customer_id}",
 				amount,
 				today.strftime("%Y%m%d")
-			)
-		])
+			),
+			incr_body(amount, body)
+		]).then { |result| { today: result[1], body: result[2] } }
+	end
+
+	def incr_body(amount, body)
+		# Short message like "hello" we can't block dupes
+		return 0 if body.to_s.length < 30
+
+		hash = Digest::SHA2.hexdigest(body[0..100])
+
+		EMPromise.all([
+			REDIS.incrby("jmp_outbound_body-#{hash}", amount),
+			REDIS.expire("jmp_outbound_body-#{hash}", 120)
+		]).then(&:first)
 	end
 
 	def message_usage(range)

sgx_jmp.rb 🔗

@@ -321,6 +321,12 @@ def billable_message(m)
 	b && !b.empty? || m.find("ns:x", ns: OOB.registered_ns).first
 end
 
+def expired_guard(customer)
+	return unless !customer.plan_name || customer.active?
+
+	raise CustomerExpired, "Your account is expired, please top up"
+end
+
 class OverLimit < StandardError
 	def initialize(customer, usage)
 		super("Please contact support")
@@ -333,7 +339,8 @@ class OverLimit < StandardError
 			BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
 			BLATHER.say(
 				CONFIG[:notify_admin], "#{@customer.customer_id} has used " \
-				"#{@usage} messages today", :groupchat
+				"#{@usage[:today]} messages today (global #{@usage[:body]} this body)",
+				:groupchat
 			)
 		end
 	end
@@ -392,7 +399,6 @@ end
 message do |m|
 	StatsD.increment("message")
 
-	today = Time.now.utc.to_date
 	find_from_and_to_customer(m.from, m.to).then { |(customer, target_customer)|
 		if target_customer && customer.registered?
 			m.from = "#{customer.registered?.phone}@sgx-jmp"
@@ -401,17 +407,18 @@ message do |m|
 
 		next customer.stanza_from(m) unless billable_message(m)
 
-		if customer.plan_name && !customer.active?
-			raise CustomerExpired, "Your account is expired, please top up"
-		end
+		expired_guard(customer)
 
 		EMPromise.all([
 			TrustLevelRepo.new.find(customer),
-			customer.message_usage((today..today))
+			customer.incr_message_usage(1, m.body)
 		]).then { |(tl, usage)|
-			raise OverLimit.new(customer, usage) unless tl.send_message?(usage)
+			next if tl.send_message?(usage[:today]) && usage[:body].to_i < 5
+
+			log.warn "OverLimit", m
+			raise OverLimit.new(customer, usage)
 		}.then do
-			EMPromise.all([customer.incr_message_usage, customer.stanza_from(m)])
+			customer.stanza_from(m)
 		end
 	}.catch_only(OverLimit) { |e|
 		e.notify_admin