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