~singpolyma/sgx-jmp

e97ecf1e21e73e43cb42c2a985a13b87ceb07379 — Stephen Paul Weber 1 year, 11 months ago a9ebca9 + 3a27852
Merge branch 'create-reset-sip-account'

* create-reset-sip-account:
  Create or reset SIP account
  Factor out Catapult connection
M Gemfile => Gemfile +1 -0
@@ 14,6 14,7 @@ gem "eventmachine"
gem "money-open-exchange-rates"
gem "ruby-bandwidth-iris"
gem "sentry-ruby"
gem "value_semantics", git: "https://github.com/singpolyma/value_semantics"

group(:development) do
	gem "pry-reload"

M config.dhall.sample => config.dhall.sample +3 -1
@@ 17,7 17,9 @@
		user = "",
		token = "",
		secret = "",
		application_id = ""
		application_id = "",
		domain = "",
		sip_host = ""
	},
	web_register = {
		to = "cheogram",

M lib/bandwidth_tn_order.rb => lib/bandwidth_tn_order.rb +5 -26
@@ 4,6 4,8 @@ require "forwardable"
require "ruby-bandwidth-iris"
Faraday.default_adapter = :em_synchrony

require_relative "./catapult"

class BandwidthTNOrder
	def self.get(id)
		EM.promise_fiber do


@@ 86,35 88,12 @@ class BandwidthTNOrder

		# After buying, import to catapult and set v1 voice app
		def catapult_import
			catapult_request.apost(
				head: catapult_headers,
				body: {
					number: tel,
					applicationId: catapult[:application_id],
					provider: dashboard_provider
				}.to_json
			)
		end

		def catapult
			CONFIG[:catapult]
		end

		def catapult_request
			EM::HttpRequest.new(
				"https://api.catapult.inetwork.com/v1/users/" \
				"#{catapult[:user]}/phoneNumbers",
				tls: { verify_peer: true }
			CATAPULT.import(
				number: tel,
				provider: dashboard_provider
			)
		end

		def catapult_headers
			{
				"Authorization" => [catapult[:token], catapult[:secret]],
				"Content-Type" => "application/json"
			}
		end

		def dashboard_provider
			{
				providerName: "bandwidth-dashboard",

A lib/catapult.rb => lib/catapult.rb +97 -0
@@ 0,0 1,97 @@
# frozen_string_literal: true

require "value_semantics/monkey_patched"

class Catapult
	value_semantics do
		user String
		token String
		secret String
		application_id String
		domain String
		sip_host String
	end

	def import(body)
		post(
			"phoneNumbers",
			body: { applicationId: application_id }.merge(body)
		)
	end

	def create_endpoint(body)
		post(
			"domains/#{@domain}/endpoints",
			body: { applicationId: @application_id }.merge(body)
		).then do |http|
			unless http.response_header.status == 201
				raise "Create new SIP account failed"
			end
			http.response_header["location"]
		end
	end

	def endpoint_list(page=0)
		get(
			"domains/#{@domain}/endpoints",
			query: { size: 1000, page: page }
		).then do |http|
			next [] if http.response_header.status == 404
			raise "Could not list endpoints" if http.response_header.status != 200

			JSON.parse(http.response)
		end
	end

	def endpoint_find(name, page=0)
		endpoint_list(page).then do |list|
			next if list.empty?

			if (found = list.find { |e| e["name"] == name })
				found.merge("url" => CATAPULT.mkurl(
					"domains/#{found['domainId']}/endpoints/#{found['id']}"
				))
			else
				endpoint_find(name, page + 1)
			end
		end
	end

	def post(path, body:, head: {})
		EM::HttpRequest.new(
			mkurl(path), tls: { verify_peer: true }
		).apost(
			head: catapult_headers.merge(head),
			body: body.to_json
		)
	end

	def delete(path, head: {})
		EM::HttpRequest.new(
			mkurl(path), tls: { verify_peer: true }
		).adelete(head: catapult_headers.merge(head))
	end

	def get(path, head: {}, **kwargs)
		EM::HttpRequest.new(
			mkurl(path), tls: { verify_peer: true }
		).aget(head: catapult_headers.merge(head), **kwargs)
	end

	def mkurl(path)
		base = "https://api.catapult.inetwork.com/v1/users/#{@user}/"
		return path if path.start_with?(base)
		"#{base}#{path}"
	end

protected

	def catapult_headers
		{
			"Authorization" => [@token, @secret],
			"Content-Type" => "application/json"
		}
	end
end

CATAPULT = Catapult.new(**CONFIG[:catapult])

M lib/customer.rb => lib/customer.rb +11 -0
@@ 9,6 9,7 @@ require_relative "./backend_sgx"
require_relative "./ibr"
require_relative "./payment_methods"
require_relative "./plan"
require_relative "./sip_account"

class Customer
	def self.for_jid(jid)


@@ 103,5 104,15 @@ class Customer
		BLATHER << @sgx.stanza(stanza)
	end

	def sip_account
		SipAccount.find(customer_id)
	end

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

	protected def_delegator :@plan, :expires_at
end

A lib/mn_words.rb => lib/mn_words.rb +277 -0
@@ 0,0 1,277 @@
# frozen_string_literal: true

MN_WORDS = [
	"academy",  "acrobat",  "active",   "actor",    "adam",     "admiral",
	"adrian",   "africa",   "agenda",   "agent",    "airline",  "airport",
	"aladdin",  "alarm",    "alaska",   "albert",   "albino",   "album",
	"alcohol",  "alex",     "algebra",  "alibi",    "alice",    "alien",
	"alpha",    "alpine",   "amadeus",  "amanda",   "amazon",   "amber",
	"america",  "amigo",    "analog",   "anatomy",  "angel",    "animal",
	"antenna",  "antonio",  "apollo",   "april",    "archive",  "arctic",
	"arizona",  "arnold",   "aroma",    "arthur",   "artist",   "asia",
	"aspect",   "aspirin",  "athena",   "athlete",  "atlas",    "audio",
	"august",   "austria",  "axiom",    "aztec",    "balance",  "ballad",
	"banana",   "bandit",   "banjo",    "barcode",  "baron",    "basic",
	"battery",  "belgium",  "berlin",   "bermuda",  "bernard",  "bikini",
	"binary",   "bingo",    "biology",  "block",    "blonde",   "bonus",
	"boris",    "boston",   "boxer",    "brandy",   "bravo",    "brazil",
	"bronze",   "brown",    "bruce",    "bruno",    "burger",   "burma",
	"cabinet",  "cactus",   "cafe",     "cairo",    "cake",     "calypso",
	"camel",    "camera",   "campus",   "canada",   "canal",    "cannon",
	"canoe",    "cantina",  "canvas",   "canyon",   "capital",  "caramel",
	"caravan",  "carbon",   "cargo",    "carlo",    "carol",    "carpet",
	"cartel",   "casino",   "castle",   "castro",   "catalog",  "caviar",
	"cecilia",  "cement",   "center",   "century",  "ceramic",  "chamber",
	"chance",   "change",   "chaos",    "charlie",  "charm",    "charter",
	"chef",     "chemist",  "cherry",   "chess",    "chicago",  "chicken",
	"chief",    "china",    "cigar",    "cinema",   "circus",   "citizen",
	"city",     "clara",    "classic",  "claudia",  "clean",    "client",
	"climax",   "clinic",   "clock",    "club",     "cobra",    "coconut",
	"cola",     "collect",  "colombo",  "colony",   "color",    "combat",
	"comedy",   "comet",    "command",  "compact",  "company",  "complex",
	"concept",  "concert",  "connect",  "consul",   "contact",  "context",
	"contour",  "control",  "convert",  "copy",     "corner",   "corona",
	"correct",  "cosmos",   "couple",   "courage",  "cowboy",   "craft",
	"crash",    "credit",   "cricket",  "critic",   "crown",    "crystal",
	"cuba",     "culture",  "dallas",   "dance",    "daniel",   "david",
	"decade",   "decimal",  "deliver",  "delta",    "deluxe",   "demand",
	"demo",     "denmark",  "derby",    "design",   "detect",   "develop",
	"diagram",  "dialog",   "diamond",  "diana",    "diego",    "diesel",
	"diet",     "digital",  "dilemma",  "diploma",  "direct",   "disco",
	"disney",   "distant",  "doctor",   "dollar",   "dominic",  "domino",
	"donald",   "dragon",   "drama",    "dublin",   "duet",     "dynamic",
	"east",     "ecology",  "economy",  "edgar",    "egypt",    "elastic",
	"elegant",  "element",  "elite",    "elvis",    "email",    "energy",
	"engine",   "english",  "episode",  "equator",  "escort",   "ethnic",
	"europe",   "everest",  "evident",  "exact",    "example",  "exit",
	"exotic",   "export",   "express",  "extra",    "fabric",   "factor",
	"falcon",   "family",   "fantasy",  "fashion",  "fiber",    "fiction",
	"fidel",    "fiesta",   "figure",   "film",     "filter",   "final",
	"finance",  "finish",   "finland",  "flash",    "florida",  "flower",
	"fluid",    "flute",    "focus",    "ford",     "forest",   "formal",
	"format",   "formula",  "fortune",  "forum",    "fragile",  "france",
	"frank",    "friend",   "frozen",   "future",   "gabriel",  "galaxy",
	"gallery",  "gamma",    "garage",   "garden",   "garlic",   "gemini",
	"general",  "genetic",  "genius",   "germany",  "global",   "gloria",
	"golf",     "gondola",  "gong",     "good",     "gordon",   "gorilla",
	"grand",    "granite",  "graph",    "green",    "group",    "guide",
	"guitar",   "guru",     "hand",     "happy",    "harbor",   "harmony",
	"harvard",  "havana",   "hawaii",   "helena",   "hello",    "henry",
	"hilton",   "history",  "horizon",  "hotel",    "human",    "humor",
	"icon",     "idea",     "igloo",    "igor",     "image",    "impact",
	"import",   "index",    "india",    "indigo",   "input",    "insect",
	"instant",  "iris",     "italian",  "jacket",   "jacob",    "jaguar",
	"janet",    "japan",    "jargon",   "jazz",     "jeep",     "john",
	"joker",    "jordan",   "jumbo",    "june",     "jungle",   "junior",
	"jupiter",  "karate",   "karma",    "kayak",    "kermit",   "kilo",
	"king",     "koala",    "korea",    "labor",    "lady",     "lagoon",
	"laptop",   "laser",    "latin",    "lava",     "lecture",  "left",
	"legal",    "lemon",    "level",    "lexicon",  "liberal",  "libra",
	"limbo",    "limit",    "linda",    "linear",   "lion",     "liquid",
	"liter",    "little",   "llama",    "lobby",    "lobster",  "local",
	"logic",    "logo",     "lola",     "london",   "lotus",    "lucas",
	"lunar",    "machine",  "macro",    "madam",    "madonna",  "madrid",
	"maestro",  "magic",    "magnet",   "magnum",   "major",    "mama",
	"mambo",    "manager",  "mango",    "manila",   "marco",    "marina",
	"market",   "mars",     "martin",   "marvin",   "master",   "matrix",
	"maximum",  "media",    "medical",  "mega",     "melody",   "melon",
	"memo",     "mental",   "mentor",   "menu",     "mercury",  "message",
	"metal",    "meteor",   "meter",    "method",   "metro",    "mexico",
	"miami",    "micro",    "million",  "mineral",  "minimum",  "minus",
	"minute",   "miracle",  "mirage",   "miranda",  "mister",   "mixer",
	"mobile",   "model",    "modem",    "modern",   "modular",  "moment",
	"monaco",   "monica",   "monitor",  "mono",     "monster",  "montana",
	"morgan",   "motel",    "motif",    "motor",    "mozart",   "multi",
	"museum",   "music",    "mustang",  "natural",  "neon",     "nepal",
	"neptune",  "nerve",    "neutral",  "nevada",   "news",     "ninja",
	"nirvana",  "normal",   "nova",     "novel",    "nuclear",  "numeric",
	"nylon",    "oasis",    "object",   "observe",  "ocean",    "octopus",
	"olivia",   "olympic",  "omega",    "opera",    "optic",    "optimal",
	"orange",   "orbit",    "organic",  "orient",   "origin",   "orlando",
	"oscar",    "oxford",   "oxygen",   "ozone",    "pablo",    "pacific",
	"pagoda",   "palace",   "pamela",   "panama",   "panda",    "panel",
	"panic",    "paradox",  "pardon",   "paris",    "parker",   "parking",
	"parody",   "partner",  "passage",  "passive",  "pasta",    "pastel",
	"patent",   "patriot",  "patrol",   "patron",   "pegasus",  "pelican",
	"penguin",  "pepper",   "percent",  "perfect",  "perfume",  "period",
	"permit",   "person",   "peru",     "phone",    "photo",    "piano",
	"picasso",  "picnic",   "picture",  "pigment",  "pilgrim",  "pilot",
	"pirate",   "pixel",    "pizza",    "planet",   "plasma",   "plaster",
	"plastic",  "plaza",    "pocket",   "poem",     "poetic",   "poker",
	"polaris",  "police",   "politic",  "polo",     "polygon",  "pony",
	"popcorn",  "popular",  "postage",  "postal",   "precise",  "prefix",
	"premium",  "present",  "price",    "prince",   "printer",  "prism",
	"private",  "product",  "profile",  "program",  "project",  "protect",
	"proton",   "public",   "pulse",    "puma",     "pyramid",  "queen",
	"radar",    "radio",    "random",   "rapid",    "rebel",    "record",
	"recycle",  "reflex",   "reform",   "regard",   "regular",  "relax",
	"report",   "reptile",  "reverse",  "ricardo",  "ringo",    "ritual",
	"robert",   "robot",    "rocket",   "rodeo",    "romeo",    "royal",
	"russian",  "safari",   "salad",    "salami",   "salmon",   "salon",
	"salute",   "samba",    "sandra",   "santana",  "sardine",  "school",
	"screen",   "script",   "second",   "secret",   "section",  "segment",
	"select",   "seminar",  "senator",  "senior",   "sensor",   "serial",
	"service",  "sheriff",  "shock",    "sierra",   "signal",   "silicon",
	"silver",   "similar",  "simon",    "single",   "siren",    "slogan",
	"social",   "soda",     "solar",    "solid",    "solo",     "sonic",
	"soviet",   "special",  "speed",    "spiral",   "spirit",   "sport",
	"static",   "station",  "status",   "stereo",   "stone",    "stop",
	"street",   "strong",   "student",  "studio",   "style",    "subject",
	"sultan",   "super",    "susan",    "sushi",    "suzuki",   "switch",
	"symbol",   "system",   "tactic",   "tahiti",   "talent",   "tango",
	"tarzan",   "taxi",     "telex",    "tempo",    "tennis",   "texas",
	"textile",  "theory",   "thermos",  "tiger",    "titanic",  "tokyo",
	"tomato",   "topic",    "tornado",  "toronto",  "torpedo",  "total",
	"totem",    "tourist",  "tractor",  "traffic",  "transit",  "trapeze",
	"travel",   "tribal",   "trick",    "trident",  "trilogy",  "tripod",
	"tropic",   "trumpet",  "tulip",    "tuna",     "turbo",    "twist",
	"ultra",    "uniform",  "union",    "uranium",  "vacuum",   "valid",
	"vampire",  "vanilla",  "vatican",  "velvet",   "ventura",  "venus",
	"vertigo",  "veteran",  "victor",   "video",    "vienna",   "viking",
	"village",  "vincent",  "violet",   "violin",   "virtual",  "virus",
	"visa",     "vision",   "visitor",  "visual",   "vitamin",  "viva",
	"vocal",    "vodka",    "volcano",  "voltage",  "volume",   "voyage",
	"water",    "weekend",  "welcome",  "western",  "window",   "winter",
	"wizard",   "wolf",     "world",    "xray",     "yankee",   "yoga",
	"yogurt",   "yoyo",     "zebra",    "zero",     "zigzag",   "zipper",
	"zodiac",   "zoom",     "abraham",  "action",   "address",  "alabama",
	"alfred",   "almond",   "ammonia",  "analyze",  "annual",   "answer",
	"apple",    "arena",    "armada",   "arsenal",  "atlanta",  "atomic",
	"avenue",   "average",  "bagel",    "baker",    "ballet",   "bambino",
	"bamboo",   "barbara",  "basket",   "bazaar",   "benefit",  "bicycle",
	"bishop",   "blitz",    "bonjour",  "bottle",   "bridge",   "british",
	"brother",  "brush",    "budget",   "cabaret",  "cadet",    "candle",
	"capitan",  "capsule",  "career",   "cartoon",  "channel",  "chapter",
	"cheese",   "circle",   "cobalt",   "cockpit",  "college",  "compass",
	"comrade",  "condor",   "crimson",  "cyclone",  "darwin",   "declare",
	"degree",   "delete",   "delphi",   "denver",   "desert",   "divide",
	"dolby",    "domain",   "domingo",  "double",   "drink",    "driver",
	"eagle",    "earth",    "echo",     "eclipse",  "editor",   "educate",
	"edward",   "effect",   "electra",  "emerald",  "emotion",  "empire",
	"empty",    "escape",   "eternal",  "evening",  "exhibit",  "expand",
	"explore",  "extreme",  "ferrari",  "first",    "flag",     "folio",
	"forget",   "forward",  "freedom",  "fresh",    "friday",   "fuji",
	"galileo",  "garcia",   "genesis",  "gold",     "gravity",  "habitat",
	"hamlet",   "harlem",   "helium",   "holiday",  "house",    "hunter",
	"ibiza",    "iceberg",  "imagine",  "infant",   "isotope",  "jackson",
	"jamaica",  "jasmine",  "java",     "jessica",  "judo",     "kitchen",
	"lazarus",  "letter",   "license",  "lithium",  "loyal",    "lucky",
	"magenta",  "mailbox",  "manual",   "marble",   "mary",     "maxwell",
	"mayor",    "milk",     "monarch",  "monday",   "money",    "morning",
	"mother",   "mystery",  "native",   "nectar",   "nelson",   "network",
	"next",     "nikita",   "nobel",    "nobody",   "nominal",  "norway",
	"nothing",  "number",   "october",  "office",   "oliver",   "opinion",
	"option",   "order",    "outside",  "package",  "pancake",  "pandora",
	"panther",  "papa",     "patient",  "pattern",  "pedro",    "pencil",
	"people",   "phantom",  "philips",  "pioneer",  "pluto",    "podium",
	"portal",   "potato",   "prize",    "process",  "protein",  "proxy",
	"pump",     "pupil",    "python",   "quality",  "quarter",  "quiet",
	"rabbit",   "radical",  "radius",   "rainbow",  "ralph",    "ramirez",
	"ravioli",  "raymond",  "respect",  "respond",  "result",   "resume",
	"retro",    "richard",  "right",    "risk",     "river",    "roger",
	"roman",    "rondo",    "sabrina",  "salary",   "salsa",    "sample",
	"samuel",   "saturn",   "savage",   "scarlet",  "scoop",    "scorpio",
	"scratch",  "scroll",   "sector",   "serpent",  "shadow",   "shampoo",
	"sharon",   "sharp",    "short",    "shrink",   "silence",  "silk",
	"simple",   "slang",    "smart",    "smoke",    "snake",    "society",
	"sonar",    "sonata",   "soprano",  "source",   "sparta",   "sphere",
	"spider",   "sponsor",  "spring",   "acid",     "adios",    "agatha",
	"alamo",    "alert",    "almanac",  "aloha",    "andrea",   "anita",
	"arcade",   "aurora",   "avalon",   "baby",     "baggage",  "balloon",
	"bank",     "basil",    "begin",    "biscuit",  "blue",     "bombay",
	"brain",    "brenda",   "brigade",  "cable",    "carmen",   "cello",
	"celtic",   "chariot",  "chrome",   "citrus",   "civil",    "cloud",
	"common",   "compare",  "cool",     "copper",   "coral",    "crater",
	"cubic",    "cupid",    "cycle",    "depend",   "door",     "dream",
	"dynasty",  "edison",   "edition",  "enigma",   "equal",    "eric",
	"event",    "evita",    "exodus",   "extend",   "famous",   "farmer",
	"food",     "fossil",   "frog",     "fruit",    "geneva",   "gentle",
	"george",   "giant",    "gilbert",  "gossip",   "gram",     "greek",
	"grille",   "hammer",   "harvest",  "hazard",   "heaven",   "herbert",
	"heroic",   "hexagon",  "husband",  "immune",   "inca",     "inch",
	"initial",  "isabel",   "ivory",    "jason",    "jerome",   "joel",
	"joshua",   "journal",  "judge",    "juliet",   "jump",     "justice",
	"kimono",   "kinetic",  "leonid",   "lima",     "maze",     "medusa",
	"member",   "memphis",  "michael",  "miguel",   "milan",    "mile",
	"miller",   "mimic",    "mimosa",   "mission",  "monkey",   "moral",
	"moses",    "mouse",    "nancy",    "natasha",  "nebula",   "nickel",
	"nina",     "noise",    "orchid",   "oregano",  "origami",  "orinoco",
	"orion",    "othello",  "paper",    "paprika",  "prelude",  "prepare",
	"pretend",  "profit",   "promise",  "provide",  "puzzle",   "remote",
	"repair",   "reply",    "rival",    "riviera",  "robin",    "rose",
	"rover",    "rudolf",   "saga",     "sahara",   "scholar",  "shelter",
	"ship",     "shoe",     "sigma",    "sister",   "sleep",    "smile",
	"spain",    "spark",    "split",    "spray",    "square",   "stadium",
	"star",     "storm",    "story",    "strange",  "stretch",  "stuart",
	"subway",   "sugar",    "sulfur",   "summer",   "survive",  "sweet",
	"swim",     "table",    "taboo",    "target",   "teacher",  "telecom",
	"temple",   "tibet",    "ticket",   "tina",     "today",    "toga",
	"tommy",    "tower",    "trivial",  "tunnel",   "turtle",   "twin",
	"uncle",    "unicorn",  "unique",   "update",   "valery",   "vega",
	"version",  "voodoo",   "warning",  "william",  "wonder",   "year",
	"yellow",   "young",    "absent",   "absorb",   "accent",   "alfonso",
	"alias",    "ambient",  "andy",     "anvil",    "appear",   "apropos",
	"archer",   "ariel",    "armor",    "arrow",    "austin",   "avatar",
	"axis",     "baboon",   "bahama",   "bali",     "balsa",    "bazooka",
	"beach",    "beast",    "beatles",  "beauty",   "before",   "benny",
	"betty",    "between",  "beyond",   "billy",    "bison",    "blast",
	"bless",    "bogart",   "bonanza",  "book",     "border",   "brave",
	"bread",    "break",    "broken",   "bucket",   "buenos",   "buffalo",
	"bundle",   "button",   "buzzer",   "byte",     "caesar",   "camilla",
	"canary",   "candid",   "carrot",   "cave",     "chant",    "child",
	"choice",   "chris",    "cipher",   "clarion",  "clark",    "clever",
	"cliff",    "clone",    "conan",    "conduct",  "congo",    "content",
	"costume",  "cotton",   "cover",    "crack",    "current",  "danube",
	"data",     "decide",   "desire",   "detail",   "dexter",   "dinner",
	"dispute",  "donor",    "druid",    "drum",     "easy",     "eddie",
	"enjoy",    "enrico",   "epoxy",    "erosion",  "except",   "exile",
	"explain",  "fame",     "fast",     "father",   "felix",    "field",
	"fiona",    "fire",     "fish",     "flame",    "flex",     "flipper",
	"float",    "flood",    "floor",    "forbid",   "forever",  "fractal",
	"frame",    "freddie",  "front",    "fuel",     "gallop",   "game",
	"garbo",    "gate",     "gibson",   "ginger",   "giraffe",  "gizmo",
	"glass",    "goblin",   "gopher",   "grace",    "gray",     "gregory",
	"grid",     "griffin",  "ground",   "guest",    "gustav",   "gyro",
	"hair",     "halt",     "harris",   "heart",    "heavy",    "herman",
	"hippie",   "hobby",    "honey",    "hope",     "horse",    "hostel",
	"hydro",    "imitate",  "info",     "ingrid",   "inside",   "invent",
	"invest",   "invite",   "iron",     "ivan",     "james",    "jester",
	"jimmy",    "join",     "joseph",   "juice",    "julius",   "july",
	"justin",   "kansas",   "karl",     "kevin",    "kiwi",     "ladder",
	"lake",     "laura",    "learn",    "legacy",   "legend",   "lesson",
	"life",     "light",    "list",     "locate",   "lopez",    "lorenzo",
	"love",     "lunch",    "malta",    "mammal",   "margo",    "marion",
	"mask",     "match",    "mayday",   "meaning",  "mercy",    "middle",
	"mike",     "mirror",   "modest",   "morph",    "morris",   "nadia",
	"nato",     "navy",     "needle",   "neuron",   "never",    "newton",
	"nice",     "night",    "nissan",   "nitro",    "nixon",    "north",
	"oberon",   "octavia",  "ohio",     "olga",     "open",     "opus",
	"orca",     "oval",     "owner",    "page",     "paint",    "palma",
	"parade",   "parent",   "parole",   "paul",     "peace",    "pearl",
	"perform",  "phoenix",  "phrase",   "pierre",   "pinball",  "place",
	"plate",    "plato",    "plume",    "pogo",     "point",    "polite",
	"polka",    "poncho",   "powder",   "prague",   "press",    "presto",
	"pretty",   "prime",    "promo",    "quasi",    "quest",    "quick",
	"quiz",     "quota",    "race",     "rachel",   "raja",     "ranger",
	"region",   "remark",   "rent",     "reward",   "rhino",    "ribbon",
	"rider",    "road",     "rodent",   "round",    "rubber",   "ruby",
	"rufus",    "sabine",   "saddle",   "sailor",   "saint",    "salt",
	"satire",   "scale",    "scuba",    "season",   "secure",   "shake",
	"shallow",  "shannon",  "shave",    "shelf",    "sherman",  "shine",
	"shirt",    "side",     "sinatra",  "sincere",  "size",     "slalom",
	"slow",     "small",    "snow",     "sofia",    "song",     "sound",
	"south",    "speech",   "spell",    "spend",    "spoon",    "stage",
	"stamp",    "stand",    "state",    "stella",   "stick",    "sting",
	"stock",    "store",    "sunday",   "sunset",   "support",  "sweden",
	"swing",    "tape",     "think",    "thomas",   "tictac",   "time",
	"toast",    "tobacco",  "tonight",  "torch",    "torso",    "touch",
	"toyota",   "trade",    "tribune",  "trinity",  "triton",   "truck",
	"trust",    "type",     "under",    "unit",     "urban",    "urgent",
	"user",     "value",    "vendor",   "venice",   "verona",   "vibrate",
	"virgo",    "visible",  "vista",    "vital",    "voice",    "vortex",
	"waiter",   "watch",    "wave",     "weather",  "wedding",  "wheel",
	"whiskey",  "wisdom",   "deal",     "null",     "nurse",    "quebec",
	"reserve",  "reunion",  "roof",     "singer",   "verbal",   "amen",
	"ego",      "fax",      "jet",      "job",      "rio",      "ski",
	"yes"
].freeze

A lib/sip_account.rb => lib/sip_account.rb +101 -0
@@ 0,0 1,101 @@
# frozen_string_literal: true

require "securerandom"
require "value_semantics/monkey_patched"

require_relative "./catapult"
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
	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
	end

	include Common

	value_semantics do
		url String
		username String
		password Either(String, nil), default: nil
	end

	def form
		form = Blather::Stanza::X.new(:result)
		form.title = "Sip Account Reset!"
		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" }
		]

		form
	end

	def put
		delete.then { create }
	end

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

			self
		end
	end

protected

	protected :url, :username, :password

	def server
		CATAPULT.sip_host
	end

	class New
		include Common

		value_semantics do
			username String
			password String, default_generator: -> { MN_WORDS.sample(3).join(" ") }
		end

		def put
			create
		end

		def with(**kwargs)
			if kwargs.key?(:url)
				SipAccount.new(internal_to_h.merge(kwargs))
			else
				super
			end
		end

		protected :username, :password
	end
end

M sgx_jmp.rb => sgx_jmp.rb +20 -0
@@ 258,6 258,11 @@ disco_items node: "http://jabber.org/protocol/commands" do |iq|
			iq.to,
			"usage",
			"Show Monthly Usage"
		),
		Blather::Stanza::DiscoItems::Item.new(
			iq.to,
			"reset sip account",
			"Create or Reset SIP Account"
		)
	]
	self << reply


@@ 321,6 326,21 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "reset sip account", sessionid: nil do |iq|
	sentry_hub = new_sentry_hub(iq, name: iq.node)
	Customer.for_jid(iq.from.stripped).then { |customer|
		sentry_hub.current_scope.set_user(
			id: customer.customer_id,
			jid: iq.from.stripped.to_s
		)
		customer.reset_sip_account
	}.then { |sip_account|
		reply = iq.reply
		reply.command << sip_account.form
		BLATHER << reply
	}.catch { |e| panic(e, sentry_hub) }
end

command :execute?, node: "usage", sessionid: nil do |iq|
	sentry_hub = new_sentry_hub(iq, name: iq.node)
	report_for = (Date.today..(Date.today << 1))

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

M test/data/catapult_import_body.json => test/data/catapult_import_body.json +1 -1
@@ 1,1 1,1 @@
{"number":"+15555550000","applicationId":"catapult_app","provider":{"providerName":"bandwidth-dashboard","properties":{"accountId":"test_bw_account","userName":"test_bw_user","password":"test_bw_password"}}}
{"applicationId":"catapult_app","number":"+15555550000","provider":{"providerName":"bandwidth-dashboard","properties":{"accountId":"test_bw_account","userName":"test_bw_user","password":"test_bw_password"}}}

M test/test_customer.rb => test/test_customer.rb +77 -0
@@ 11,6 11,14 @@ 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_for_jid
		Customer::REDIS.expect(


@@ 205,4 213,73 @@ class CustomerTest < Minitest::Test
		)
	end
	em :test_customer_usage_report

	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"
		).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
			}
		).to_return(status: 404)
		sip = Customer.new("test").sip_account.sync
		assert_kind_of SipAccount::New, sip
		assert_equal "test", sip.username
		assert_requested req
	end
	em :test_sip_account_new

	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"
		).with(
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0"
			}
		).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: [
			{ name: "test", domainId: "domain", id: "endpoint" }
		].to_json)

		sip = Customer.new("test").sip_account.sync
		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)

		assert_raises(RuntimeError) do
			Customer.new("test").sip_account.sync
		end
	end
	em :test_sip_account_error
end

M test/test_helper.rb => test/test_helper.rb +2 -0
@@ 48,6 48,8 @@ CONFIG = {
		user: "catapult_user",
		token: "catapult_token",
		secret: "catapult_secret",
		domain: "catapult_domain",
		sip_host: "host.bwapp.io.example.com",
		application_id: "catapult_app"
	},
	activation_amount: 1,

A test/test_sip_account.rb => test/test_sip_account.rb +121 -0
@@ 0,0 1,121 @@
# frozen_string_literal: true

require "test_helper"
require "sip_account"

class SipAccount
	public :password, :url

	class New
		public :password
	end
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"
		)
	end

	def test_with_random_password
		new_sip = @sip.with_random_password
		refute_equal @sip.password, new_sip.password
		refute_empty new_sip.password
		assert_kind_of String, new_sip.password
	end

	def test_form
		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
	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"
		).with(
			body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
			headers: {
				"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
				"Content-Type" => "application/json"
			}
		).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
	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)
		stub_request(
			:post,
			"https://api.catapult.inetwork.com/v1/users/" \
			"catapult_user/domains/catapult_domain/endpoints"
		).to_return(status: 400)
		assert_raises(RuntimeError) { @sip.put.sync }
	end
	em :test_put_post_fail

	class NewTest < Minitest::Test
		def setup
			@sip = SipAccount::New.new(
				username: "12345",
				password: "old password"
			)
		end

		def test_with_random_password
			new_sip = @sip.with_random_password
			refute_equal @sip.password, new_sip.password
			refute_empty new_sip.password
			assert_kind_of String, new_sip.password
		end

		def test_put
			post = stub_request(
				:post,
				"https://api.catapult.inetwork.com/v1/users/" \
				"catapult_user/domains/catapult_domain/endpoints"
			).with(
				body: open(__dir__ + "/data/catapult_create_sip.json").read.chomp,
				headers: {
					"Authorization" => "Basic Y2F0YXB1bHRfdG9rZW46Y2F0YXB1bHRfc2VjcmV0",
					"Content-Type" => "application/json"
				}
			).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 post
		end
		em :test_put
	end
end