backport: i love censorship

Phillip Davis created

Change summary

sgx-bwmsgsv2.rb                | 51 ++++++++++++++++++++
test/property/test_badwords.rb | 91 ++++++++++++++++++++++++++++++++++++
2 files changed, 142 insertions(+)

Detailed changes

sgx-bwmsgsv2.rb 🔗

@@ -46,6 +46,49 @@ require_relative 'lib/registration_repo'
 
 Sentry.init
 
+BADWORD_LIST = [
+	"marijuana",
+	"psilocybin",
+	"cannabis",
+	"cocaine",
+	"heroin",
+	"meth",
+	"methamphetamine",
+	"methamphetamines",
+	"cigarette",
+	"tobacco",
+	"cbd",
+	"thc",
+	"morphine",
+	"incall",
+	"in-call",
+	"outcall",
+	"out-call",
+	"shrooms",
+	"lsd",
+	"kratom",
+	"mdma",
+	"addy",
+	"xanz",
+	"cialis",
+	"viagra",
+	"bbfs",
+	"fentanyl",
+	"opium",
+	"golden teacher",
+	"bbbj",
+	"canna",
+	"fuck",
+	"xanax",
+	"zarareturns",
+	"zarareturns.com",
+	"plantation",
+].freeze
+
+BADWORDS = Regexp.union(
+	BADWORD_LIST.map { |w| /\b#{Regexp.escape(w)}\b/ }
+)
+
 # List of supported MIME types from Bandwidth - https://support.bandwidth.com/hc/en-us/articles/360014128994-What-MMS-file-types-are-supported-
 MMS_MIME_TYPES = [
 	"application/json",
@@ -390,6 +433,14 @@ module SGXbwmsgsv2
 			)
 		end
 
+		if body.downcase.match?(BADWORDS)
+			return EMPromise.reject([
+				:wait,
+				'recipient-unavailable',
+				'Single message blocked by carrier content policy, see https://blog.jmp.chat/b/sms-censorship for details'
+			])
+		end
+
 		segment_size = body.ascii_only? ? 160 : 70
 		if !murl && ENV["MMS_PATH"] && body.length > segment_size*3
 			file = Multibases.pack(

test/property/test_badwords.rb 🔗

@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "../../sgx-bwmsgsv2"
+require "rantly/minitest_extensions"
+require_relative "rantly_extensions/data_extensions"
+
+class BadwordsPropertyTest < Minitest::Test
+	BW_MESSAGES_URL =
+		"https://messaging.bandwidth.com/api/v2/users/account/messages"
+
+	def setup
+		reset_stanzas!
+		reset_redis!
+	end
+
+	def test_message_containing_badword_is_rejected
+		property_of {
+			bad_word = choose(*BADWORD_LIST)
+			cased = choose(bad_word.downcase, bad_word.upcase, bad_word.capitalize)
+
+			prefix = array(range(0, 4)) { sized(range(3, 8)) { string(:alnum) } }
+			suffix = array(range(0, 4)) { sized(range(3, 8)) { string(:alnum) } }
+			body = (prefix + [cased] + suffix).join(" ")
+
+			dest = nanpa_phone
+			[body, dest]
+		}.check { |body, dest|
+			reset_stanzas!
+			reset_redis!
+
+			stub_request(:post, BW_MESSAGES_URL).to_return(
+				status: 201,
+				body: JSON.dump(id: "bw-msg-stub")
+			)
+
+			m = Blather::Stanza::Message.new("#{dest}@component", body)
+			m.from = "test@example.com"
+			process_stanza(m)
+
+			assert_equal 1, written.length,
+			             "Expected exactly one error stanza for body: #{body.inspect}"
+
+			stanza = Blather::XMPPNode.parse(written.first.to_xml)
+			assert stanza.error?,
+			       "Expected error stanza for body: #{body.inspect}"
+
+			error = stanza.find_first("error")
+			assert_equal "wait", error["type"],
+			             "Expected error type 'wait' for body: #{body.inspect}"
+			assert_equal "recipient-unavailable", xmpp_error_name(error),
+			             "Expected 'recipient-unavailable' for body: #{body.inspect}"
+		}
+	end
+	em :test_message_containing_badword_is_rejected
+
+	def test_message_without_badwords_is_not_rejected_as_unavailable
+		property_of {
+			words = array(range(1, 6)) { sized(range(3, 10)) { string(:alnum) } }
+			guard(words.none? { |w| BADWORD_LIST.include?(w.downcase) })
+			body = words.join(" ")
+
+			dest = nanpa_phone
+			[body, dest]
+		}.check { |body, dest|
+			reset_stanzas!
+			reset_redis!
+
+			stub_request(:post, BW_MESSAGES_URL).to_return(
+				status: 201,
+				body: JSON.dump(id: "bw-msg-stub")
+			)
+
+			m = Blather::Stanza::Message.new("#{dest}@component", body)
+			m.from = "test@example.com"
+			process_stanza(m)
+
+			written.each do |response|
+				stanza = Blather::XMPPNode.parse(response.to_xml)
+				next unless stanza.error?
+
+				error = stanza.find_first("error")
+				msg = "Clean message rejected as " \
+					"recipient-unavailable: #{body.inspect}"
+				refute_equal "recipient-unavailable",
+				             xmpp_error_name(error), msg
+			end
+		}
+	end
+	em :test_message_without_badwords_is_not_rejected_as_unavailable
+end