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