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 [](jid)
20 @redis.get("pending_tel_for-#{jid}").then do |tel|
21 tel ? HaveTel.new(tel) : ChooseTel.new
22 end
23 end
24
25 class HaveTel
26 def initialize(tel)
27 @tel = tel
28 end
29
30 def choose_tel
31 EMPromise.resolve(@tel)
32 end
33 end
34
35 class ChooseTel
36 def choose_tel(error: nil)
37 Command.reply { |reply|
38 reply.allowed_actions = [:next]
39 reply.command << FormTemplate.render("tn_search", error: error)
40 }.then { |iq| choose_from_list(AvailableNumber.for(iq.form).tns) }
41 end
42
43 def choose_from_list(tns)
44 if tns.empty?
45 choose_tel(error: "No numbers found, try another search.")
46 else
47 Command.reply { |reply|
48 reply.allowed_actions = [:next]
49 reply.command << FormTemplate.render("tn_list", tns: tns)
50 }.then { |iq| iq.form.field("tel").value.to_s.strip }
51 end
52 end
53
54 class AvailableNumber
55 def self.for(form)
56 new(
57 Q
58 .for(form.field("q").value.to_s.strip).iris_query
59 .merge(enableTNDetail: true)
60 .merge(Quantity.for(form).iris_query)
61 )
62 end
63
64 def initialize(iris_query)
65 @iris_query = iris_query
66 end
67
68 def tns
69 Command.log.debug("BandwidthIris::AvailableNumber.list", @iris_query)
70 BandwidthIris::AvailableNumber.list(@iris_query).map(&Tn.method(:new))
71 end
72
73 class Quantity
74 def self.for(form)
75 rsm_max = form.find(
76 "ns:set/ns:max",
77 ns: "http://jabber.org/protocol/rsm"
78 ).first
79 if rsm_max
80 new(rsm_max.content.to_i)
81 else
82 Default.new
83 end
84 end
85
86 def initialize(quantity)
87 @quantity = quantity
88 end
89
90 def iris_query
91 { quantity: @quantity }
92 end
93
94 # NOTE: Gajim sends back the whole list on submit, so big
95 # lists can cause issues
96 class Default
97 def iris_query
98 { quantity: 10 }
99 end
100 end
101 end
102 end
103
104 class Tn
105 attr_reader :tel
106
107 def initialize(full_number:, city:, state:, **)
108 @tel = "+1#{full_number}"
109 @locality = city
110 @region = state
111 end
112
113 def formatted_tel
114 @tel =~ /\A\+1(\d{3})(\d{3})(\d+)\Z/
115 "(#{$1}) #{$2}-#{$3}"
116 end
117
118 def option
119 op = Blather::Stanza::X::Field::Option.new(value: tel, label: to_s)
120 op << reference
121 op
122 end
123
124 def reference
125 Nokogiri::XML::Builder.new { |xml|
126 xml.reference(
127 xmlns: "urn:xmpp:reference:0",
128 begin: 0,
129 end: formatted_tel.length - 1,
130 type: "data",
131 uri: "tel:#{tel}"
132 )
133 }.doc.root
134 end
135
136 def to_s
137 "#{formatted_tel} (#{@locality}, #{@region})"
138 end
139 end
140
141 class Q
142 def self.register(regex, &block)
143 @queries ||= []
144 @queries << [regex, block]
145 end
146
147 def self.for(q)
148 @queries.each do |(regex, block)|
149 match_data = (q =~ regex)
150 return block.call($1 || $&, *$~.to_a[2..-1]) if match_data
151 end
152
153 raise "Format not recognized: #{q}"
154 end
155
156 def initialize(q)
157 @q = q
158 end
159
160 {
161 areaCode: [:AreaCode, /\A[2-9][0-9]{2}\Z/],
162 npaNxx: [:NpaNxx, /\A(?:[2-9][0-9]{2}){2}\Z/],
163 npaNxxx: [:NpaNxxx, /\A(?:[2-9][0-9]{2}){2}[0-9]\Z/],
164 zip: [:PostalCode, /\A\d{5}(?:-\d{4})?\Z/],
165 localVanity: [:LocalVanity, /\A~(.+)\Z/]
166 }.each do |k, args|
167 klass = const_set(
168 args[0],
169 Class.new(Q) do
170 define_method(:iris_query) do
171 { k => @q }
172 end
173 end
174 )
175
176 args[1..-1].each do |regex|
177 register(regex) { |q| klass.new(q) }
178 end
179 end
180
181 class CityState
182 Q.register(/\A([^,]+)\s*,\s*([A-Z]{2})\Z/, &method(:new))
183 def initialize(city, state)
184 @city = city
185 @state = state
186 end
187
188 def iris_query
189 { city: @city, state: @state }
190 end
191 end
192 end
193 end
194end