1# frozen_string_literal: true
2
3require "pg/em/connection_pool"
4require "bandwidth"
5require "bigdecimal"
6require "blather/client/dsl" # Require this first to not auto-include
7require "blather/client"
8require "braintree"
9require "date"
10require "dhall"
11require "em-hiredis"
12require "em_promise"
13require "ougai"
14require "ruby-bandwidth-iris"
15require "sentry-ruby"
16require "statsd-instrument"
17
18$stdout.sync = true
19LOG = Ougai::Logger.new($stdout)
20LOG.level = ENV.fetch("LOG_LEVEL", "info")
21LOG.formatter = Ougai::Formatters::Readable.new(
22 nil,
23 nil,
24 plain: !$stdout.isatty
25)
26Blather.logger = LOG
27EM::Hiredis.logger = LOG
28StatsD.logger = LOG
29LOG.info "Starting"
30
31Sentry.init do |config|
32 config.logger = LOG
33 config.breadcrumbs_logger = [:sentry_logger]
34end
35
36module SentryOugai
37 class SentryLogger
38 include Sentry::Breadcrumb::SentryLogger
39 include Singleton
40 end
41
42 def _log(severity, message=nil, ex=nil, data=nil, &block)
43 super
44 SentryLogger.instance.add_breadcrumb(severity, message || ex.to_s, &block)
45 end
46end
47LOG.extend SentryOugai
48
49CONFIG = Dhall::Coder
50 .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
51 .load(
52 "(#{ARGV[0]}) : #{__dir__}/config-schema.dhall",
53 transform_keys: ->(k) { k&.to_sym }
54 )
55WEB_LISTEN =
56 if CONFIG[:web].is_a?(Hash)
57 [CONFIG[:web][:interface], CONFIG[:web][:port]]
58 else
59 [CONFIG[:web]]
60 end
61
62singleton_class.class_eval do
63 include Blather::DSL
64 Blather::DSL.append_features(self)
65end
66
67require_relative "lib/polyfill"
68require_relative "lib/alt_top_up_form"
69require_relative "lib/admin_command"
70require_relative "lib/add_bitcoin_address"
71require_relative "lib/backend_sgx"
72require_relative "lib/bwmsgsv2_repo"
73require_relative "lib/bandwidth_iris_patch"
74require_relative "lib/bandwidth_tn_order"
75require_relative "lib/bandwidth_tn_repo"
76require_relative "lib/btc_sell_prices"
77require_relative "lib/buy_account_credit_form"
78require_relative "lib/configure_calls_form"
79require_relative "lib/command"
80require_relative "lib/command_list"
81require_relative "lib/customer"
82require_relative "lib/customer_info_form"
83require_relative "lib/customer_repo"
84require_relative "lib/dummy_command"
85require_relative "lib/db_notification"
86require_relative "lib/electrum"
87require_relative "lib/empty_repo"
88require_relative "lib/expiring_lock"
89require_relative "lib/em"
90require_relative "lib/form_to_h"
91require_relative "lib/low_balance"
92require_relative "lib/port_in_order"
93require_relative "lib/payment_methods"
94require_relative "lib/paypal_done"
95require_relative "lib/postgres"
96require_relative "lib/registration"
97require_relative "lib/transaction"
98require_relative "lib/tel_selections"
99require_relative "lib/session_manager"
100require_relative "lib/statsd"
101require_relative "web"
102
103ELECTRUM = Electrum.new(**CONFIG[:electrum])
104EM::Hiredis::Client.load_scripts_from("./redis_lua")
105
106Faraday.default_adapter = :em_synchrony
107BandwidthIris::Client.global_options = {
108 account_id: CONFIG[:creds][:account],
109 username: CONFIG[:creds][:username],
110 password: CONFIG[:creds][:password]
111}
112BANDWIDTH_VOICE = Bandwidth::Client.new(
113 voice_basic_auth_user_name: CONFIG[:creds][:username],
114 voice_basic_auth_password: CONFIG[:creds][:password]
115).voice_client.client
116
117def new_sentry_hub(stanza, name: nil)
118 hub = Sentry.get_current_hub&.new_from_top
119 raise "Sentry.init has not been called" unless hub
120
121 hub.push_scope
122 hub.current_scope.clear_breadcrumbs
123 hub.current_scope.set_transaction_name(name) if name
124 hub.current_scope.set_user(jid: stanza.from.stripped.to_s)
125 hub
126end
127
128class AuthError < StandardError; end
129
130# Braintree is not async, so wrap in EM.defer for now
131class AsyncBraintree
132 def initialize(environment:, merchant_id:, public_key:, private_key:, **)
133 @gateway = Braintree::Gateway.new(
134 environment: environment,
135 merchant_id: merchant_id,
136 public_key: public_key,
137 private_key: private_key
138 )
139 @gateway.config.logger = LOG
140 end
141
142 def respond_to_missing?(m, *)
143 @gateway.respond_to?(m) || super
144 end
145
146 def method_missing(m, *args)
147 return super unless respond_to_missing?(m, *args)
148
149 EM.promise_defer(klass: PromiseChain) do
150 @gateway.public_send(m, *args)
151 end
152 end
153
154 class PromiseChain < EMPromise
155 def respond_to_missing?(*)
156 false && super # We don't actually know what we respond to...
157 end
158
159 def method_missing(m, *args)
160 return super if respond_to_missing?(m, *args)
161
162 self.then { |o| o.public_send(m, *args) }
163 end
164 end
165end
166
167BRAINTREE = AsyncBraintree.new(**CONFIG[:braintree])
168
169def panic(e, hub=nil)
170 (Thread.current[:log] || LOG).fatal(
171 "Error raised during event loop: #{e.class}",
172 e
173 )
174 if e.is_a?(::Exception)
175 (hub || Sentry).capture_exception(e, hint: { background: false })
176 else
177 (hub || Sentry).capture_message(e.to_s, hint: { background: false })
178 end
179 exit 1
180end
181
182EM.error_handler(&method(:panic))
183
184# Infer anything we might have been notified about while we were down
185def catchup_notify(db)
186 db.query("SELECT customer_id FROM balances WHERE balance < 5").each do |c|
187 db.query("SELECT pg_notify('low_balance', $1)", c.values)
188 end
189 db.query(<<~SQL).each do |c|
190 SELECT customer_id
191 FROM customer_plans INNER JOIN balances USING (customer_id)
192 WHERE expires_at < LOCALTIMESTAMP AND balance >= 5
193 SQL
194 db.query("SELECT pg_notify('possible_renewal', $1)", c.values)
195 end
196end
197
198def poll_for_notify(db)
199 db.wait_for_notify_defer.then { |notify|
200 repo = CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
201 repo.find(notify[:extra]).then do |customer|
202 DbNotification.for(notify, customer, repo)
203 end
204 }.then(&:call).then {
205 poll_for_notify(db)
206 }.catch(&method(:panic))
207end
208
209def load_plans_to_db!
210 DB.transaction do
211 DB.exec("TRUNCATE plans")
212 CONFIG[:plans].each do |plan|
213 DB.exec("INSERT INTO plans VALUES ($1)", [plan.to_json])
214 end
215 end
216end
217
218when_ready do
219 LOG.info "Ready"
220 BLATHER = self
221 REDIS = EM::Hiredis.connect
222 TEL_SELECTIONS = TelSelections.new
223 BTC_SELL_PRICES = BTCSellPrices.new(REDIS, CONFIG[:oxr_app_id])
224 DB = Postgres.connect(dbname: "jmp")
225
226 DB.hold do |conn|
227 conn.query("LISTEN low_balance")
228 conn.query("LISTEN possible_renewal")
229 catchup_notify(conn)
230 poll_for_notify(conn)
231 end
232
233 load_plans_to_db!
234
235 EM.add_periodic_timer(3600) do
236 ping = Blather::Stanza::Iq::Ping.new(:get, CONFIG[:server][:host])
237 ping.from = CONFIG[:component][:jid]
238 self << ping
239 end
240
241 Web.run(LOG.child, *WEB_LISTEN)
242end
243
244# workqueue_count MUST be 0 or else Blather uses threads!
245setup(
246 CONFIG[:component][:jid],
247 CONFIG[:component][:secret],
248 CONFIG[:server][:host],
249 CONFIG[:server][:port],
250 nil,
251 nil,
252 workqueue_count: 0
253)
254
255message to: /\Aaccount@/, body: /./ do |m|
256 StatsD.increment("deprecated_account_bot")
257
258 self << m.reply.tap { |out|
259 out.body = "This bot is deprecated. Please talk to xmpp:cheogram.com"
260 }
261end
262
263before(
264 :iq,
265 type: [:error, :result],
266 to: /\Acustomer_/,
267 from: /(\A|@)#{CONFIG[:sgx]}(\/|\Z)/
268) { |iq| halt if IQ_MANAGER.fulfill(iq) }
269
270before nil, to: /\Acustomer_/, from: /(\A|@)#{CONFIG[:sgx]}(\/|\Z)/ do |s|
271 StatsD.increment("stanza_customer")
272
273 sentry_hub = new_sentry_hub(s, name: "stanza_customer")
274 CustomerRepo.new(set_user: sentry_hub.current_scope.method(:set_user)).find(
275 s.to.node.delete_prefix("customer_")
276 ).then { |customer|
277 customer.stanza_to(s)
278 }.catch { |e| panic(e, sentry_hub) }
279 halt
280end
281
282ADDRESSES_NS = "http://jabber.org/protocol/address"
283message(
284 to: /\A#{CONFIG[:component][:jid]}\Z/,
285 from: /(\A|@)#{CONFIG[:sgx]}(\/|\Z)/
286) do |m|
287 StatsD.increment("inbound_group_text")
288 sentry_hub = new_sentry_hub(m, name: "inbound_group_text")
289
290 address = m.find("ns:addresses", ns: ADDRESSES_NS).first
291 &.find("ns:address", ns: ADDRESSES_NS)
292 &.find { |el| el["jid"].to_s.start_with?("customer_") }
293 pass unless address
294
295 CustomerRepo
296 .new(set_user: sentry_hub.current_scope.method(:set_user))
297 .find_by_jid(address["jid"]).then { |customer|
298 m.from = m.from.with(domain: CONFIG[:component][:jid])
299 m.to = m.to.with(domain: customer.jid.domain)
300 address["jid"] = customer.jid.to_s
301 BLATHER << m
302 }.catch_only(CustomerRepo::NotFound) { |e|
303 BLATHER << m.as_error("forbidden", :auth, e.message)
304 }.catch { |e| panic(e, sentry_hub) }
305end
306
307# Ignore groupchat messages
308# Especially if we have the component join MUC for notifications
309message(type: :groupchat) { true }
310
311UNBILLED_TARGETS = Set.new(CONFIG[:unbilled_targets])
312def billable_message(m)
313 b = m.body
314 !UNBILLED_TARGETS.member?(m.to.node) && \
315 (b && !b.empty? || m.find("ns:x", ns: OOB.registered_ns).first)
316end
317
318class OverLimit < StandardError
319 def initialize(customer, usage)
320 super("Please contact support")
321 @customer = customer
322 @usage = usage
323 end
324
325 def notify_admin
326 ExpiringLock.new("jmp_usage_notify-#{@customer.customer_id}").with do
327 BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
328 BLATHER.say(
329 CONFIG[:notify_admin], "#{@customer.customer_id} has used " \
330 "#{@usage} messages today", :groupchat
331 )
332 end
333 end
334end
335
336class CustomerExpired < StandardError; end
337
338message do |m|
339 StatsD.increment("message")
340
341 sentry_hub = new_sentry_hub(m, name: "message")
342 today = Time.now.utc.to_date
343 CustomerRepo.new(set_user: sentry_hub.current_scope.method(:set_user))
344 .find_by_jid(m.from.stripped).then { |customer|
345 next customer.stanza_from(m) unless billable_message(m)
346
347 if customer.plan_name && !customer.active?
348 raise CustomerExpired, "Your account is expired, please top up"
349 end
350
351 EMPromise.all([
352 TrustLevelRepo.new.find(customer),
353 customer.message_usage((today..today))
354 ]).then { |(tl, usage)|
355 raise OverLimit.new(customer, usage) unless tl.send_message?(usage)
356 }.then do
357 EMPromise.all([
358 customer.incr_message_usage, customer.stanza_from(m)
359 ])
360 end
361 }.catch_only(OverLimit) { |e|
362 e.notify_admin
363 BLATHER << m.as_error("policy-violation", :wait, e.message)
364 }.catch_only(CustomerRepo::NotFound, CustomerExpired) { |e|
365 BLATHER << m.as_error("forbidden", :auth, e.message)
366 }.catch { |e| panic(e, sentry_hub) }
367end
368
369message :error? do |m|
370 StatsD.increment("message_error")
371
372 LOG.error "MESSAGE ERROR", stanza: m
373end
374
375IQ_MANAGER = SessionManager.new(self, :id)
376COMMAND_MANAGER = SessionManager.new(
377 self,
378 :sessionid,
379 timeout: 60 * 60,
380 error_if: ->(s) { s.cancel? }
381)
382
383disco_info to: Blather::JID.new(CONFIG[:component][:jid]) do |iq|
384 reply = iq.reply
385 reply.identities = [{
386 name: "JMP.chat",
387 type: "sms",
388 category: "gateway"
389 }]
390 reply.features = [
391 "http://jabber.org/protocol/disco#info",
392 "http://jabber.org/protocol/commands"
393 ]
394 form = Blather::Stanza::X.find_or_create(reply.query)
395 form.type = "result"
396 form.fields = [
397 {
398 var: "FORM_TYPE",
399 type: "hidden",
400 value: "http://jabber.org/network/serverinfo"
401 }
402 ] + CONFIG[:xep0157]
403 self << reply
404end
405
406disco_info do |iq|
407 reply = iq.reply
408 reply.identities = [{
409 name: "JMP.chat",
410 type: "sms",
411 category: "client"
412 }]
413 reply.features = [
414 "urn:xmpp:receipts"
415 ]
416 self << reply
417end
418
419disco_items node: "http://jabber.org/protocol/commands" do |iq|
420 StatsD.increment("command_list")
421
422 sentry_hub = new_sentry_hub(iq, name: iq.node)
423 reply = iq.reply
424 reply.node = "http://jabber.org/protocol/commands"
425
426 CustomerRepo.new(
427 sgx_repo: Bwmsgsv2Repo.new,
428 set_user: sentry_hub.current_scope.method(:set_user)
429 ).find_by_jid(
430 iq.from.stripped
431 ).catch {
432 nil
433 }.then { |customer|
434 CommandList.for(customer, iq.from)
435 }.then { |list|
436 reply.items = list.map { |item|
437 Blather::Stanza::DiscoItems::Item.new(
438 iq.to,
439 item[:node],
440 item[:name]
441 )
442 }
443 self << reply
444 }.catch { |e| panic(e, sentry_hub) }
445end
446
447iq "/iq/ns:services", ns: "urn:xmpp:extdisco:2" do |iq|
448 StatsD.increment("extdisco")
449
450 reply = iq.reply
451 reply << Nokogiri::XML::Builder.new {
452 services(xmlns: "urn:xmpp:extdisco:2") do
453 service(
454 type: "sip",
455 host: CONFIG[:sip_host]
456 )
457 end
458 }.doc.root
459
460 self << reply
461end
462
463Command.new(
464 "jabber:iq:register",
465 "Register",
466 list_for: ->(*) { true },
467 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
468) {
469 Command.customer.catch_only(CustomerRepo::NotFound) {
470 Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Customer.create"))
471 Command.execution.customer_repo.create(Command.execution.iq.from.stripped)
472 }.then { |customer|
473 Sentry.add_breadcrumb(Sentry::Breadcrumb.new(message: "Registration.for"))
474 Registration.for(customer, TEL_SELECTIONS).then(&:write)
475 }.then {
476 StatsD.increment("registration.completed")
477 }.catch_only(Command::Execution::FinalStanza) do |e|
478 StatsD.increment("registration.completed")
479 EMPromise.reject(e)
480 end
481}.register(self).then(&CommandList.method(:register))
482
483Command.new(
484 "info",
485 "Show Account Info",
486 list_for: ->(*) { true },
487 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
488) {
489 Command.customer.then(&:info).then do |info|
490 Command.finish do |reply|
491 reply.command << info.form
492 end
493 end
494}.register(self).then(&CommandList.method(:register))
495
496Command.new(
497 "usage",
498 "Show Monthly Usage"
499) {
500 report_for = (Date.today..(Date.today << 1))
501
502 Command.customer.then { |customer|
503 customer.usage_report(report_for)
504 }.then do |usage_report|
505 Command.finish do |reply|
506 reply.command << usage_report.form
507 end
508 end
509}.register(self).then(&CommandList.method(:register))
510
511Command.new(
512 "transactions",
513 "Show Transactions",
514 list_for: ->(customer:, **) { !!customer&.currency }
515) {
516 Command.customer.then(&:transactions).then do |txs|
517 Command.finish do |reply|
518 reply.command << FormTemplate.render("transactions", transactions: txs)
519 end
520 end
521}.register(self).then(&CommandList.method(:register))
522
523Command.new(
524 "configure calls",
525 "Configure Calls",
526 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
527) {
528 Command.customer.then do |customer|
529 cc_form = ConfigureCallsForm.new(customer)
530 Command.reply { |reply|
531 reply.allowed_actions = [:next]
532 reply.command << cc_form.render
533 }.then { |iq|
534 EMPromise.all(cc_form.parse(iq.form).map { |k, v|
535 Command.execution.customer_repo.public_send("put_#{k}", customer, v)
536 })
537 }.then { Command.finish("Configuration saved!") }
538 end
539}.register(self).then(&CommandList.method(:register))
540
541Command.new(
542 "ogm",
543 "Record Voicemail Greeting",
544 list_for: ->(fwd: nil, **) { !!fwd },
545 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
546) {
547 Command.customer.then do |customer|
548 customer.fwd.create_call(CONFIG[:creds][:account]) do |cc|
549 cc.from = customer.registered?.phone
550 cc.application_id = CONFIG[:sip][:app]
551 cc.answer_url = "#{CONFIG[:web_root]}/ogm/start?" \
552 "customer_id=#{customer.customer_id}"
553 end
554 Command.finish("You will now receive a call.")
555 end
556}.register(self).then(&CommandList.method(:register))
557
558Command.new(
559 "migrate billing",
560 "Switch from PayPal or expired trial to new billing",
561 list_for: ->(tel:, customer:, **) { tel && !customer&.currency },
562 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
563) {
564 EMPromise.all([
565 Command.customer,
566 Command.reply do |reply|
567 reply.allowed_actions = [:next]
568 reply.command << FormTemplate.render("migrate_billing")
569 end
570 ]).then do |(customer, iq)|
571 Registration::Payment.for(
572 iq, customer, customer.registered?.phone,
573 final_message: PaypalDone::MESSAGE,
574 finish: PaypalDone
575 ).then(&:write).catch_only(Command::Execution::FinalStanza) do |s|
576 BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
577 BLATHER.say(
578 CONFIG[:notify_admin],
579 "#{customer.customer_id} migrated to #{customer.currency}",
580 :groupchat
581 )
582 EMPromise.reject(s)
583 end
584 end
585}.register(self).then(&CommandList.method(:register))
586
587Command.new(
588 "credit cards",
589 "Credit Card Settings and Management"
590) {
591 Command.customer.then do |customer|
592 url = CONFIG[:credit_card_url].call(
593 customer.jid.to_s.gsub("\\", "%5C"),
594 customer.customer_id
595 )
596 desc = "Manage credits cards and settings"
597 Command.finish("#{desc}: #{url}") do |reply|
598 oob = OOB.find_or_create(reply.command)
599 oob.url = url
600 oob.desc = desc
601 end
602 end
603}.register(self).then(&CommandList.method(:register))
604
605Command.new(
606 "top up",
607 "Buy Account Credit by Credit Card",
608 list_for: ->(payment_methods: [], **) { !payment_methods.empty? },
609 format_error: ->(e) { "Failed to buy credit, system said: #{e.message}" }
610) {
611 Command.customer.then { |customer|
612 BuyAccountCreditForm.for(customer).then do |credit_form|
613 Command.reply { |reply|
614 reply.allowed_actions = [:complete]
615 credit_form.add_to_form(reply.form)
616 }.then do |iq|
617 Transaction.sale(customer, **credit_form.parse(iq.form))
618 end
619 end
620 }.then { |transaction|
621 transaction.insert.then do
622 Command.finish("#{transaction} added to your account balance.")
623 end
624 }.catch_only(BuyAccountCreditForm::AmountValidationError) do |e|
625 Command.finish(e.message, type: :error)
626 end
627}.register(self).then(&CommandList.method(:register))
628
629Command.new(
630 "alt top up",
631 "Buy Account Credit by Bitcoin, Mail, or Interac e-Transfer",
632 list_for: ->(customer:, **) { !!customer&.currency }
633) {
634 Command.customer.then { |customer|
635 EMPromise.all([AltTopUpForm.for(customer), customer])
636 }.then do |(alt_form, customer)|
637 Command.reply { |reply|
638 reply.allowed_actions = [:complete]
639 reply.command << alt_form.form
640 }.then do |iq|
641 AddBitcoinAddress.for(iq, alt_form, customer).write
642 end
643 end
644}.register(self).then(&CommandList.method(:register))
645
646Command.new(
647 "plan settings",
648 "Manage your plan, including overage limits",
649 list_for: ->(customer:, **) { !!customer&.currency }
650) {
651 Command.customer.then do |customer|
652 Command.reply { |reply|
653 reply.allowed_actions = [:next]
654 reply.command << FormTemplate.render("plan_settings", customer: customer)
655 }.then { |iq|
656 Command.execution.customer_repo.put_monthly_overage_limit(
657 customer,
658 iq.form.field("monthly_overage_limit")&.value.to_i
659 )
660 }.then { Command.finish("Configuration saved!") }
661 end
662}.register(self).then(&CommandList.method(:register))
663
664Command.new(
665 "referral codes",
666 "Refer a friend for free credit"
667) {
668 Command.customer.then(&:unused_invites).then do |invites|
669 if invites.empty?
670 Command.finish("You have no more invites right now, try again later.")
671 else
672 Command.finish do |reply|
673 reply.form.type = :result
674 reply.form.title = "Unused Invite Codes"
675 reply.form.instructions =
676 "Each of these codes is single use and gives the person using " \
677 "them a free month of JMP service. You will receive credit " \
678 "equivalent to one month of free service if they later become " \
679 "a paying customer."
680 FormTable.new(
681 invites.map { |i| [i] },
682 code: "Invite Code"
683 ).add_to_form(reply.form)
684 end
685 end
686 end
687}.register(self).then(&CommandList.method(:register))
688
689Command.new(
690 "reset sip account",
691 "Create or Reset SIP Account",
692 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
693) {
694 Command.customer.then do |customer|
695 sip_account = customer.reset_sip_account
696 Command.reply { |reply|
697 reply.allowed_actions = [:next]
698 form = sip_account.form
699 form.type = :form
700 form.fields += [{
701 type: :boolean, var: "change_fwd",
702 label: "Should inbound calls forward to this SIP account?"
703 }]
704 reply.command << form
705 }.then do |fwd|
706 if ["1", "true"].include?(fwd.form.field("change_fwd")&.value.to_s)
707 Command.execution.customer_repo.put_fwd(
708 customer,
709 customer.fwd.with(uri: sip_account.uri)
710 ).then { Command.finish("Inbound calls will now forward to SIP.") }
711 else
712 Command.finish
713 end
714 end
715 end
716}.register(self).then(&CommandList.method(:register))
717
718Command.new(
719 "lnp",
720 "Port in your number from another carrier",
721 list_for: ->(**) { true }
722) {
723 using FormToH
724
725 EMPromise.all([
726 Command.customer,
727 Command.reply do |reply|
728 reply.allowed_actions = [:next]
729 reply.command << FormTemplate.render("lnp")
730 end
731 ]).then do |(customer, iq)|
732 order = PortInOrder.new(iq.form.to_h.slice(
733 "BillingTelephoneNumber", "Subscriber", "WirelessInfo"
734 ).merge("CustomerOrderId" => customer.customer_id))
735 order_id = BandwidthIris::PortIn.create(order.to_h)[:order_id]
736 url = "https://dashboard.bandwidth.com/portal/r/a/" \
737 "#{CONFIG[:creds][:account]}/orders/portIn/#{order_id}"
738 BLATHER.join(CONFIG[:notify_admin], "sgx-jmp")
739 BLATHER.say(
740 CONFIG[:notify_admin],
741 "New port-in request for #{customer.customer_id}: #{url}",
742 :groupchat
743 )
744 Command.finish(
745 "Your port-in request has been accepted, " \
746 "support will contact you with next steps"
747 )
748 end
749}.register(self).then(&CommandList.method(:register))
750
751Command.new(
752 "customer info",
753 "Show Customer Info",
754 list_for: ->(customer: nil, **) { customer&.admin? }
755) {
756 Command.customer.then do |customer|
757 raise AuthError, "You are not an admin" unless customer&.admin?
758
759 customer_repo = CustomerRepo.new(
760 sgx_repo: Bwmsgsv2Repo.new,
761 bandwidth_tn_repo: EmptyRepo.new(BandwidthTnRepo.new) # No CNAM in admin
762 )
763
764 Command.reply { |reply|
765 reply.allowed_actions = [:next]
766 reply.command << FormTemplate.render("customer_picker")
767 }.then { |response|
768 CustomerInfoForm.new(customer_repo).find_customer(response)
769 }.then do |target_customer|
770 AdminCommand.new(target_customer, customer_repo).start
771 end
772 end
773}.register(self).then(&CommandList.method(:register))
774
775def reply_with_note(iq, text, type: :info)
776 reply = iq.reply
777 reply.status = :completed
778 reply.note_type = type
779 reply.note_text = text
780
781 self << reply
782end
783
784Command.new(
785 "https://ns.cheogram.com/sgx/jid-switch",
786 "Change JID",
787 list_for: ->(customer: nil, **) { customer },
788 customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
789) {
790 Command.customer.then { |customer|
791 Command.reply { |reply|
792 reply.command << FormTemplate.render("jid_switch")
793 }.then { |response|
794 new_jid = response.form.field("jid").value
795 repo = Command.execution.customer_repo
796 repo.find_by_jid(new_jid)
797 .catch_only(CustomerRepo::NotFound) { nil }
798 .then { |cust|
799 next EMPromise.reject("Customer Already Exists") if cust
800
801 repo.change_jid(customer, new_jid)
802 }
803 }.then {
804 StatsD.increment("changejid.completed")
805 Command.finish { |reply|
806 reply.note_type = :info
807 reply.note_text = "Customer JID Changed"
808 }
809 }
810 }
811}.register(self).then(&CommandList.method(:register))
812
813Command.new(
814 "web-register",
815 "Initiate Register from Web",
816 list_for: lambda { |from_jid: nil, **|
817 from_jid&.stripped.to_s == CONFIG[:web_register][:from]
818 }
819) {
820 if Command.execution.iq.from.stripped != CONFIG[:web_register][:from]
821 next EMPromise.reject(
822 Command::Execution::FinalStanza.new(iq.as_error("forbidden", :auth))
823 )
824 end
825
826 Command.reply { |reply|
827 reply.command << FormTemplate.render("web_register")
828 }.then do |iq|
829 jid = iq.form.field("jid")&.value.to_s.strip
830 tel = iq.form.field("tel")&.value.to_s.strip
831 if jid !~ /\./
832 Command.finish("The Jabber ID you entered was not valid.", type: :error)
833 elsif tel !~ /\A\+\d+\Z/
834 Command.finish("Invalid telephone number", type: :error)
835 else
836 IQ_MANAGER.write(Blather::Stanza::Iq::Command.new.tap { |cmd|
837 cmd.to = CONFIG[:web_register][:to]
838 cmd.node = "push-register"
839 cmd.form.fields = [{ var: "to", value: jid }]
840 cmd.form.type = "submit"
841 }).then { |result|
842 TEL_SELECTIONS.set(result.form.field("from")&.value.to_s.strip, tel)
843 }.then { Command.finish }
844 end
845 end
846}.register(self).then(&CommandList.method(:register))
847
848command sessionid: /./ do |iq|
849 COMMAND_MANAGER.fulfill(iq)
850end
851
852iq type: [:result, :error] do |iq|
853 IQ_MANAGER.fulfill(iq)
854end
855
856iq type: [:get, :set] do |iq|
857 StatsD.increment("unknown_iq")
858
859 self << Blather::StanzaError.new(iq, "feature-not-implemented", :cancel)
860end