tel_selections.rb

  1# frozen_string_literal: true
  2
  3require "ruby-bandwidth-iris"
  4Faraday.default_adapter = :em_synchrony
  5
  6require_relative "form_template"
  7
  8class TelSelections
  9	THIRTY_DAYS = 60 * 60 * 24 * 30
 10
 11	def initialize(redis: REDIS)
 12		@redis = redis
 13	end
 14
 15	def set(jid, tel)
 16		@redis.setex("pending_tel_for-#{jid}", THIRTY_DAYS, tel)
 17	end
 18
 19	def delete(jid)
 20		@redis.del("pending_tel_for-#{jid}")
 21	end
 22
 23	def [](jid)
 24		@redis.get("pending_tel_for-#{jid}").then do |tel|
 25			tel ? HaveTel.new(tel) : ChooseTel.new
 26		end
 27	end
 28
 29	class HaveTel
 30		def initialize(tel)
 31			@tel = tel
 32		end
 33
 34		def choose_tel
 35			EMPromise.resolve(@tel)
 36		end
 37	end
 38
 39	class ChooseTel
 40		def choose_tel(error: nil)
 41			Command.reply { |reply|
 42				reply.allowed_actions = [:next]
 43				reply.command << FormTemplate.render("tn_search", error: error)
 44			}.then do |iq|
 45				choose_from_list(AvailableNumber.for(iq.form).tns)
 46			rescue StandardError
 47				choose_tel(error: $!.to_s)
 48			end
 49		end
 50
 51		def choose_from_list(tns)
 52			raise "No numbers found, try another search." if tns.empty?
 53
 54			Command.reply { |reply|
 55				reply.allowed_actions = [:next, :prev]
 56				reply.command << FormTemplate.render("tn_list", tns: tns)
 57			}.then { |iq|
 58				tel = iq.form.field("tel")&.value
 59				next choose_tel if iq.prev? || !tel
 60
 61				tel.to_s.strip
 62			}
 63		end
 64
 65		class AvailableNumber
 66			def self.for(form)
 67				new(
 68					Q
 69					.for(form.field("q").value.to_s.strip).iris_query
 70					.merge(enableTNDetail: true, LCA: false)
 71					.merge(Quantity.for(form).iris_query)
 72				)
 73			end
 74
 75			def initialize(iris_query)
 76				@iris_query = iris_query
 77			end
 78
 79			def tns
 80				Command.log.debug("BandwidthIris::AvailableNumber.list", @iris_query)
 81				BandwidthIris::AvailableNumber.list(@iris_query).map do |tn|
 82					Tn.new(**tn)
 83				end
 84			end
 85
 86			class Quantity
 87				def self.for(form)
 88					rsm_max = form.find(
 89						"ns:set/ns:max",
 90						ns: "http://jabber.org/protocol/rsm"
 91					).first
 92					if rsm_max
 93						new(rsm_max.content.to_i)
 94					else
 95						Default.new
 96					end
 97				end
 98
 99				def initialize(quantity)
100					@quantity = quantity
101				end
102
103				def iris_query
104					{ quantity: @quantity }
105				end
106
107				# NOTE: Gajim sends back the whole list on submit, so big
108				# lists can cause issues
109				class Default
110					def iris_query
111						{ quantity: 10 }
112					end
113				end
114			end
115		end
116
117		class Tn
118			attr_reader :tel
119
120			def initialize(full_number:, city:, state:, **)
121				@tel = "+1#{full_number}"
122				@locality = city
123				@region = state
124			end
125
126			def formatted_tel
127				@tel =~ /\A\+1(\d{3})(\d{3})(\d+)\Z/
128				"(#{$1}) #{$2}-#{$3}"
129			end
130
131			def option
132				op = Blather::Stanza::X::Field::Option.new(value: tel, label: to_s)
133				op << reference
134				op
135			end
136
137			def reference
138				Nokogiri::XML::Builder.new { |xml|
139					xml.reference(
140						xmlns: "urn:xmpp:reference:0",
141						begin: 0,
142						end: formatted_tel.length - 1,
143						type: "data",
144						uri: "tel:#{tel}"
145					)
146				}.doc.root
147			end
148
149			def to_s
150				"#{formatted_tel} (#{@locality}, #{@region})"
151			end
152		end
153
154		class Q
155			def self.register(regex, &block)
156				@queries ||= []
157				@queries << [regex, block]
158			end
159
160			def self.for(q)
161				@queries.each do |(regex, block)|
162					match_data = (q =~ regex)
163					return block.call($1 || $&, *$~.to_a[2..-1]) if match_data
164				end
165
166				raise "Format not recognized: #{q}"
167			end
168
169			def initialize(q)
170				@q = q
171			end
172
173			{
174				areaCode: [:AreaCode, /\A[2-9][0-9]{2}\Z/],
175				npaNxx: [:NpaNxx, /\A(?:[2-9][0-9]{2}){2}\Z/],
176				npaNxxx: [:NpaNxxx, /\A(?:[2-9][0-9]{2}){2}[0-9]\Z/],
177				zip: [:PostalCode, /\A\d{5}(?:-\d{4})?\Z/],
178				localVanity: [:LocalVanity, /\A~(.+)\Z/]
179			}.each do |k, args|
180				klass = const_set(
181					args[0],
182					Class.new(Q) {
183						define_method(:iris_query) do
184							{ k => @q }
185						end
186					}
187				)
188
189				args[1..-1].each do |regex|
190					register(regex) { |q| klass.new(q) }
191				end
192			end
193
194			class State
195				Q.register(/\A[a-zA-Z]{2}\Z/, &method(:new))
196
197				STATE_MAP = {
198					"QC" => "PQ"
199				}.freeze
200
201				def initialize(state)
202					@state = STATE_MAP.fetch(state.upcase, state.upcase)
203				end
204
205				def iris_query
206					{ state: @state }
207				end
208			end
209
210			class CityState
211				Q.register(/\A([^,]+)\s*,\s*([a-zA-Z]{2})\Z/, &method(:new))
212
213				CITY_MAP = {
214					"ajax" => "Ajax-Pickering",
215					"kitchener" => "Kitchener-Waterloo",
216					"new york" => "New York City",
217					"pickering" => "Ajax-Pickering",
218					"sault ste marie" => "sault sainte marie",
219					"sault ste. marie" => "sault sainte marie",
220					"south durham" => "Durham",
221					"township of langley" => "Langley",
222					"waterloo" => "Kitchener-Waterloo",
223					"west durham" => "Durham"
224				}.freeze
225
226				def initialize(city, state)
227					@city = CITY_MAP.fetch(city.downcase, city)
228					@state = State.new(state)
229				end
230
231				def iris_query
232					@state.iris_query.merge(city: @city)
233				end
234			end
235		end
236	end
237end