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