Notify admin if a user goes over 500 messages in 30 days

Stephen Paul Weber created

Notify only once per day (using expiring redis key).
Can notify MUC or user, always sends directed presence first so it will join MUC
if not joined.
Ignore all messages direct to the component, mostly to throw out live messages
from MUC if we join that to notify.

Change summary

config.dhall.sample   |  1 +
lib/customer.rb       |  2 +-
lib/customer_usage.rb | 30 +++++++++++++++++++++++++++++-
sgx_jmp.rb            | 33 +++++++++++++++++++++------------
4 files changed, 52 insertions(+), 14 deletions(-)

Detailed changes

config.dhall.sample 🔗

@@ -39,6 +39,7 @@
 	xep0157 = [
 		{ var = "support-addresses", value = "xmpp:+14169938000@cheogram.com" }
 	],
+	notify_admin = "muc_or_user@example.com",
 	sip_host = "sip.jmp.chat",
 	plans = [
 		{

lib/customer.rb 🔗

@@ -49,7 +49,7 @@ class Customer
 	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
 	               :currency, :merchant_account, :plan_name
 	def_delegators :@sgx, :register!, :registered?
-	def_delegator :@usage, :report, :usage_report
+	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
 
 	def initialize(
 		customer_id,

lib/customer_usage.rb 🔗

@@ -7,7 +7,7 @@ class CustomerUsage
 		@customer_id = customer_id
 	end
 
-	def report(range)
+	def usage_report(range)
 		EMPromise.all([
 			messages_by_day(range),
 			minutes_by_day(range)
@@ -16,6 +16,34 @@ class CustomerUsage
 		end
 	end
 
+	def expire_message_usage
+		today = Time.now.utc.to_date
+		REDIS.zremrangebylex(
+			"jmp_customer_outbound_messages-#{@customer_id}",
+			"-",
+			# Store message counts per day for 1 year
+			"[#{(today << 12).strftime('%Y%m%d')}"
+		)
+	end
+
+	def incr_message_usage(amount=1)
+		today = Time.now.utc.to_date
+		EMPromise.all([
+			expire_message_usage,
+			REDIS.zincrby(
+				"jmp_customer_outbound_messages-#{@customer_id}",
+				amount,
+				today.strftime("%Y%m%d")
+			)
+		])
+	end
+
+	def message_usage(range)
+		messages_by_day(range).then do |by_day|
+			by_day.values.sum
+		end
+	end
+
 	def messages_by_day(range)
 		EMPromise.all(range.first.downto(range.last).map { |day|
 			REDIS.zscore(

sgx_jmp.rb 🔗

@@ -163,28 +163,37 @@ before nil, to: /\Acustomer_/, from: /(\A|@)#{CONFIG[:sgx]}(\/|\Z)/ do |s|
 	halt
 end
 
+# Ignore messages to component
+# Especially if we have the component join MUC for notifications
+message(to: /\A#{CONFIG[:component][:jid]}\Z/) {}
+
 message do |m|
 	sentry_hub = new_sentry_hub(m, name: "message")
+	today = Time.now.utc.to_date
 	Customer.for_jid(m.from.stripped).then { |customer|
 		sentry_hub.current_scope.set_user(
 			id: customer.customer_id,
 			jid: m.from.stripped.to_s
 		)
-		today = Time.now.utc.to_date
 		EMPromise.all([
-			REDIS.zremrangebylex(
-				"jmp_customer_outbound_messages-#{customer.customer_id}",
-				"-",
-				# Store message counts per day for 1 year
-				"[#{(today << 12).strftime('%Y%m%d')}"
-			),
-			REDIS.zincrby(
-				"jmp_customer_outbound_messages-#{customer.customer_id}",
-				1,
-				today.strftime("%Y%m%d")
-			),
+			customer,
+			customer.incr_message_usage,
+			REDIS.exists("jmp_usage_notify-#{customer.customer_id}"),
 			customer.stanza_from(m)
 		])
+	}.then { |(customer, _, already, _)|
+		next if already == 1
+
+		customer.message_usage((today..(today - 30))).then do |usage|
+			next unless usage > 500
+
+			BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
+			BLATHER.say(
+				CONFIG[:notify_admin],
+				"#{customer.customer_id} has used #{usage} messages since #{today - 30}"
+			)
+			REDIS.set("jmp_usage_notify-#{customer.customer_id}", ex: 60 * 60 * 24)
+		end
 	}.catch { |e| panic(e, sentry_hub) }
 end