~singpolyma/sgx-jmp

93b7deabd48edac68279b68f9d85705f604f136b — Stephen Paul Weber 1 year, 7 months ago 7f5da6b + 1510063
Merge branch 'reset-sip-v2'

* reset-sip-v2:
  After SIP reset, offer to change inbound fwd
  Reset sip account using v2 API
M Gemfile => Gemfile +1 -1
@@ 18,7 18,7 @@ gem "multibases"
gem "multihashes"
gem "ougai"
gem "roda"
gem "ruby-bandwidth-iris"
gem "ruby-bandwidth-iris", git: "https://github.com/singpolyma/ruby-bandwidth-iris", branch: "sip_credential"
gem "sentry-ruby", "<= 4.3.1"
gem "slim"
gem "statsd-instrument", git: "https://github.com/singpolyma/statsd-instrument.git", branch: "graphite"

M config-schema.dhall => config-schema.dhall +1 -1
@@ 1,7 1,6 @@
{ activation_amount : Natural
, admins : List Text
, adr : Text
, bandwidth_app : Text
, bandwidth_peer : Text
, bandwidth_site : Text
, braintree :


@@ 44,6 43,7 @@
      }
, server : { host : Text, port : Natural }
, sgx : Text
, sip : { app : Text, realm : Text }
, sip_host : Text
, upstream_domain : Text
, web : < Inet : { interface : Text, port : Natural } | Unix : Text >

M config.dhall.sample => config.dhall.sample +4 -1
@@ 33,7 33,6 @@ in
	},
	bandwidth_site = "",
	bandwidth_peer = "",
	bandwidth_app = "", -- This can be any voice app
	braintree = {
		environment = "sandbox",
		merchant_id = "",


@@ 47,6 46,10 @@ in
	xep0157 = [
		{ var = "support-addresses", value = "xmpp:+14169938000@cheogram.com", label = "Support" }
	],
	sip = {
		realm = "",
		app = ""
	},
	notify_admin = "muc@example.com",
	sip_host = "sip.jmp.chat",
	plans = [

M lib/backend_sgx.rb => lib/backend_sgx.rb +7 -1
@@ 24,7 24,9 @@ class BackendSgx
		ibr.username = creds[:username]
		ibr.password = creds[:password]
		ibr.phone = tel
		IQ_MANAGER.write(ibr)
		IQ_MANAGER.write(ibr).then do
			with(registered?: irb)
		end
	end

	def stanza(s)


@@ 34,6 36,10 @@ class BackendSgx
		end
	end

	def set_fwd(uri)
		REDIS.set("catapult_fwd-#{registered?.phone}", uri)
	end

	def set_fwd_timeout(timeout)
		REDIS.set("catapult_fwd_timeout-#{from_jid}", timeout)
	end

M lib/customer.rb => lib/customer.rb +2 -4
@@ 23,7 23,7 @@ class Customer
	def_delegators :@plan, :active?, :activate_plan_starting_now, :bill_plan,
	               :currency, :merchant_account, :plan_name, :auto_top_up_amount
	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
	               :set_fwd_timeout, :fwd, :transcription_enabled
	               :set_fwd, :fwd, :transcription_enabled
	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage

	def initialize(


@@ 92,9 92,7 @@ class Customer
	end

	def reset_sip_account
		SipAccount::New.new(username: customer_id).put.catch do
			sip_account.then { |acct| acct.with_random_password.put }
		end
		sip_account.with_random_password.put
	end

	def btc_addresses

M lib/registration.rb => lib/registration.rb +3 -3
@@ 459,11 459,11 @@ class Registration
		end

		def customer_active_tel_purchased
			@customer.register!(@tel).catch(&method(:raise_setup_error)).then {
			@customer.register!(@tel).catch(&method(:raise_setup_error)).then { |sgx|
				EMPromise.all([
					REDIS.del("pending_tel_for-#{@customer.jid}"),
					REDIS.set("catapult_fwd-#{@tel}", cheogram_sip_addr),
					@customer.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
					sgx.set_fwd(cheogram_sip_addr),
					sgx.set_fwd_timeout(25) # ~5 seconds / ring, 5 rings
				])
			}.then do
				Command.finish("Your JMP account has been activated as #{@tel}")

M lib/sip_account.rb => lib/sip_account.rb +49 -61
@@ 1,44 1,38 @@
# frozen_string_literal: true

require "em-synchrony/em-http" # For aget vs get
require "digest"
require "securerandom"
require "value_semantics/monkey_patched"

require_relative "./catapult"
require_relative "./mn_words"
require_relative "mn_words"

class SipAccount
	def self.find(name)
		CATAPULT.endpoint_find(name).then do |found|
			next New.new(username: name) unless found

			new(username: found["name"], url: found["url"])
		end
		new(BandwidthIris::SipCredential.get(name))
	rescue BandwidthIris::Errors::GenericError # 404
		New.new(BandwidthIris::SipCredential.new(
			user_name: name,
			realm: CONFIG[:sip][:realm],
			http_voice_v2_app_id: CONFIG[:sip][:app]
		))
	end

	module Common
		def with_random_password
			with(password: MN_WORDS.sample(3).join(" "))
		end

	protected

		def create
			CATAPULT.create_endpoint(
				name: username,
				credentials: { password: password }
			).then do |url|
				with(url: url)
			end
		end
	def initialize(api_object, password: nil)
		@api_object = api_object
		@password = password
	end

	include Common
	def with(password:)
		self.class.new(@api_object.class.new(@api_object.to_data.merge(
			hash1: Digest::MD5.hexdigest("#{username}:#{server}:#{password}"),
			hash1b: Digest::MD5.hexdigest(
				"#{username}:#{server}:#{server}:#{password}"
			)
		)), password: password)
	end

	value_semantics do
		url String
		username String
		password Either(String, nil), default: nil
	def with_random_password
		with(password: MN_WORDS.sample(3).join(" "))
	end

	def form


@@ 47,56 41,50 @@ class SipAccount
		form.instructions = "These are your new SIP credentials"

		form.fields = [
			{ var: "username", value: username, label: "Username" },
			{ var: "password", value: password, label: "Password" },
			{ var: "server", value: server, label: "Server" }
			{ var: "username", value: username, label: "Username", type: :fixed },
			{ var: "password", value: @password, label: "Password", type: :fixed },
			{ var: "server", value: server, label: "Server", type: :fixed }
		]

		form
	end

	def put
		delete.then { create }
		@api_object.update(
			hash1: @api_object.hash1,
			hash1b: @api_object.hash1b,
			realm: server,
			http_voice_v2_app_id: @api_object.http_voice_v2_app_id
		)
		self
	end

	def delete
		CATAPULT.delete(url).then do |http|
			unless http.response_header.status == 200
				raise "Delete old SIP account failed"
			end

			self
		end
		@api_object.delete
	end

protected

	protected :url, :username, :password
	def username
		@api_object.user_name.to_s
	end

	def server
		CATAPULT.sip_host
		@api_object.realm
	end

	class New
		include Common

		value_semantics do
			username String
			password String, default_generator: -> { MN_WORDS.sample(3).join(" ") }
		end
	def uri
		"sip:#{username}@#{server}"
	end

	class New < SipAccount
		def put
			create
		end

		def with(**kwargs)
			if kwargs.key?(:url)
				SipAccount.new(internal_to_h.merge(kwargs))
			else
				super
			end
			BandwidthIris::SipCredential.create(
				user_name: username,
				hash1: @api_object.hash1,
				hash1b: @api_object.hash1b,
				realm: server,
				http_voice_v2_app_id: @api_object.http_voice_v2_app_id
			)
			self
		end

		protected :username, :password
	end
end

M sgx_jmp.rb => sgx_jmp.rb +27 -5
@@ 475,7 475,7 @@ Command.new(
			CONFIG[:creds][:account],
			body: customer.fwd.create_call_request do |cc|
				cc.from = customer.registered?.phone
				cc.application_id = CONFIG[:bandwidth_app]
				cc.application_id = CONFIG[:sip][:app]
				cc.answer_url = "#{CONFIG[:web_root]}/ogm/start?" \
				                "customer_id=#{customer.customer_id}"
			end


@@ 599,11 599,33 @@ Command.new(

Command.new(
	"reset sip account",
	"Create or Reset SIP Account"
	"Create or Reset SIP Account",
	customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
) {
	Command.customer.then(&:reset_sip_account).then do |sip_account|
		Command.finish do |reply|
			reply.command << sip_account.form
	Command.customer.then do |customer|
		sip_account = customer.reset_sip_account
		Command.reply { |reply|
			reply.allowed_actions = [:next]
			form = sip_account.form
			form.type = :form
			form.fields += [{
				type: :boolean, var: "change_fwd",
				label: "Should inbound calls forward to this SIP account?"
			}]
			reply.command << form
		}.then do |fwd|
			if ["1", "true"].include?(fwd.form.field("change_fwd")&.value.to_s)
				# Migrate location if needed
				BandwidthIris::SipPeer.new(
					site_id: CONFIG[:bandwidth_site],
					id: CONFIG[:bandwidth_peer]
				).move_tns([customer.registered?.phone])
				customer.set_fwd(sip_account.uri).then do
					Command.finish("Inbound calls will now forward to SIP.")
				end
			else
				Command.finish
			end
		end
	end
}.register(self).then(&CommandList.method(:register))

D test/data/catapult_create_sip.json => test/data/catapult_create_sip.json +0 -1
@@ 1,1 0,0 @@
{"applicationId":"catapult_app","name":"12345","credentials":{"password":"old password"}}

M test/test_customer.rb => test/test_customer.rb +14 -42
@@ 13,14 13,6 @@ CustomerPlan::DB = Minitest::Mock.new
CustomerUsage::REDIS = Minitest::Mock.new
CustomerUsage::DB = Minitest::Mock.new

class SipAccount
	public :username, :url

	class New
		public :username
	end
end

class CustomerTest < Minitest::Test
	def test_bill_plan_activate
		CustomerPlan::DB.expect(:transaction, nil) do |&block|


@@ 152,14 144,13 @@ class CustomerTest < Minitest::Test
	def test_sip_account_new
		req = stub_request(
			:get,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
		).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
				"Authorization" => "Basic Og=="
			}
		).to_return(status: 404)
		sip = customer.sip_account.sync
		sip = customer.sip_account
		assert_kind_of SipAccount::New, sip
		assert_equal "test", sip.username
		assert_requested req


@@ 169,52 160,33 @@ class CustomerTest < Minitest::Test
	def test_sip_account_existing
		req1 = stub_request(
			:get,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
		).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
				"Authorization" => "Basic Og=="
			}
		).to_return(status: 200, body: [
			{ name: "NOTtest", domainId: "domain", id: "endpoint" }
		].to_json)

		req2 = stub_request(
			:get,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints?page=1&size=1000"
		).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
		).to_return(status: 200, body: {
			SipCredential: {
				UserName: "test",
				Realm: "sip.example.com"
			}
		).to_return(status: 200, body: [
			{ name: "test", domainId: "domain", id: "endpoint" }
		].to_json)
		}.to_xml)

		sip = customer.sip_account.sync
		sip = customer.sip_account
		assert_kind_of SipAccount, sip
		assert_equal "test", sip.username
		assert_equal(
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/domain/endpoints/endpoint",
			sip.url
		)

		assert_requested req1
		assert_requested req2
	end
	em :test_sip_account_existing

	def test_sip_account_error
		stub_request(
			:get,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints?page=0&size=1000"
		).to_return(status: 400)
			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/test"
		).to_return(status: 404)

		assert_raises(RuntimeError) do
			customer.sip_account.sync
		end
		assert_equal "test", customer.sip_account.username
	end
	em :test_sip_account_error


M test/test_helper.rb => test/test_helper.rb +4 -0
@@ 94,6 94,10 @@ CONFIG = {
			USD: "merchant_usd"
		}
	},
	sip: {
		realm: "sip.example.com",
		app: "sipappid"
	},
	credit_card_url: ->(*) { "http://creditcard.example.com" },
	electrum_notify_url: ->(*) { "http://notify.example.com" },
	upstream_domain: "example.net"

M test/test_registration.rb => test/test_registration.rb +8 -6
@@ 564,6 564,11 @@ class RegistrationTest < Minitest::Test
				}
			).to_return(status: 201)
			Registration::Finish::REDIS.expect(
				:del,
				nil,
				["pending_tel_for-test@example.net"]
			)
			BackendSgx::REDIS.expect(
				:set,
				nil,
				[


@@ 571,11 576,6 @@ class RegistrationTest < Minitest::Test
					"sip:test%40example.net@sip.cheogram.com"
				]
			)
			Registration::Finish::REDIS.expect(
				:del,
				nil,
				["pending_tel_for-test@example.net"]
			)
			BackendSgx::REDIS.expect(
				:set,
				nil,


@@ 597,7 597,9 @@ class RegistrationTest < Minitest::Test
			execute_command(blather: blather) do
				@sgx.expect(
					:register!,
					EMPromise.resolve(OpenStruct.new(error?: false)),
					EMPromise.resolve(@sgx.with(registered?: IBR.new.tap do |ibr|
						ibr.phone = "+15555550000"
					end)),
					["+15555550000"]
				)


M test/test_sip_account.rb => test/test_sip_account.rb +45 -57
@@ 4,21 4,18 @@ require "test_helper"
require "sip_account"

class SipAccount
	public :password, :url

	class New
		public :password
	end
	attr_reader :password
end

class SipAccountTest < Minitest::Test
	def setup
		@sip = SipAccount.new(
			url: "https://api.catapult.inetwork.com/v1/" \
			     "users/catapult_user/domains/catapult_domain/endpoints/test",
			username: "12345",
			password: "old password"
		)
			BandwidthIris::SipCredential.new(
				user_name: "12345",
				realm: "sip.example.com",
				http_voice_v2_app_id: "sipappid"
			)
		).with(password: "old password")
	end

	def test_with_random_password


@@ 32,61 29,52 @@ class SipAccountTest < Minitest::Test
		form = @sip.form
		assert_equal "12345", form.field("username").value
		assert_equal "old password", form.field("password").value
		assert_equal "host.bwapp.io.example.com", form.field("server").value
		assert_equal "sip.example.com", form.field("server").value
	end

	def test_put
		delete = stub_request(:delete, @sip.url).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
			}
		).to_return(status: 200)

		post = stub_request(
			:post,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints"
		put = stub_request(
			:put,
			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
		).with(
			body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
			body: {
				Hash1: "73b05bcaf9096438c978aecff5f7cc45",
				Hash1b: "2b7fe68f6337ef4db29e752684a18db4",
				Realm: "sip.example.com",
				HttpVoiceV2AppId: "sipappid"
			}.to_xml(indent: 0, root: "SipCredential"),
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
				"Content-Type" => "application/json"
				"Authorization" => "Basic Og=="
			}
		).to_return(
			status: 201,
			headers: { "Location" => "http://example.com/endpoint" }
		)

		new_sip = @sip.put.sync
		assert_equal "http://example.com/endpoint", new_sip.url
		assert_requested delete
		assert_requested post
		new_sip = @sip.put
		assert_equal "12345", new_sip.username
		assert_requested put
	end
	em :test_put

	def test_put_delete_fail
		stub_request(:delete, @sip.url).to_return(status: 400)
		assert_raises(RuntimeError) { @sip.put.sync }
	end
	em :test_put_delete_fail

	def test_put_post_fail
		stub_request(:delete, @sip.url).to_return(status: 200)
	def test_put_fail
		stub_request(
			:post,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints"
			:put,
			"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
		).to_return(status: 400)
		assert_raises(RuntimeError) { @sip.put.sync }
		assert_raises(BandwidthIris::Errors::GenericError) { @sip.put }
	end
	em :test_put_post_fail
	em :test_put_fail

	class NewTest < Minitest::Test
		def setup
			@sip = SipAccount::New.new(
				username: "12345",
				password: "old password"
			)
			@sip = SipAccount.new(
				BandwidthIris::SipCredential.new(
					user_name: "12345",
					realm: "sip.example.com",
					http_voice_v2_app_id: "sipappid"
				)
			).with(password: "old password")
		end

		def test_with_random_password


@@ 98,22 86,22 @@ class SipAccountTest < Minitest::Test

		def test_put
			post = stub_request(
				:post,
				"https://api.catapult.inetwork.com/v1/users/" \
				"catapult_user/domains/catapult_domain/endpoints"
				:put,
				"https://dashboard.bandwidth.com/v1.0/accounts//sipcredentials/12345"
			).with(
				body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
				body: {
					Hash1: "73b05bcaf9096438c978aecff5f7cc45",
					Hash1b: "2b7fe68f6337ef4db29e752684a18db4",
					Realm: "sip.example.com",
					HttpVoiceV2AppId: "sipappid"
				}.to_xml(indent: 0, root: "SipCredential"),
				headers: {
					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
					"Content-Type" => "application/json"
					"Authorization" => "Basic Og=="
				}
			).to_return(
				status: 201,
				headers: { "Location" => "http://example.com/endpoint" }
			)
			).to_return(status: 201)

			new_sip = @sip.put.sync
			assert_equal "http://example.com/endpoint", new_sip.url
			new_sip = @sip.put
			assert_equal "12345", new_sip.username
			assert_requested post
		end
		em :test_put