sgx_jmp.rb

  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