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