~singpolyma/sgx-jmp

3a278525ea8243c6073099490d23d079eb5fa255 — Stephen Paul Weber 1 year, 11 months ago 4178a87
Create or reset SIP account

New command to create or reset SIP account.  Always try create first because
it's faster and more common, fall back to search the list for our account if
that fails due to conflict.  Password is always randomly generated from the
mnemonicode word list.
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/catapult.rb => lib/catapult.rb +52 -0
@@ 8,6 8,8 @@ class Catapult
		token String
		secret String
		application_id String
		domain String
		sip_host String
	end

	def import(body)


@@ 17,6 19,44 @@ class Catapult
		)
	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 }


@@ 26,6 66,18 @@ class Catapult
		)
	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)

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/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