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