diff --git a/bvs.js b/bvs.js new file mode 100644 index 0000000..685e11b --- /dev/null +++ b/bvs.js @@ -0,0 +1,193 @@ +async function getPremiumDIDs() { + return new Promise((resolve, reject) => { + fetch(`https://portal.bulkvs.com/api/v1.0/tnRecord?Status=Active&Trunk%20Group=${process.env.TRUNK_GROUP}`, { + headers: { + "Authorization": `Bearer ${process.env.BVS_TOKEN}`, + } + }) + .then(res => { + if (!res.ok) { + throw new Error(`Error fetching DIDs: ${res.status} ${res.statusText}`); + } + return res.json(); + }) + .then(data => { + // data.forEach(record => { + // console.log(record.ReferenceID); + // }); + // data is array of objects. If object.ReferenceID.split(";;")[0] is premDID, then [1] is the user id, and anything after is a note + const dids = data.filter(record => record.ReferenceID && record.ReferenceID.startsWith(";;")).map(record => { + // Get all flags (just keep looking at the next split item until its not a valid flag, after that, it's all a custom note, most likely to be used fo) + let validFlags = ["bypassNitroReq"]; + let flags = []; + let note = ""; + let splitReference = record.ReferenceID.split(";;"); + for (let i = 1; i < splitReference.length; i++) { + if (validFlags.includes(splitReference[i])) { + flags.push(splitReference[i]); + } + } + return { + did: record.TN, + userId: record.ReferenceID.split(";;")[1], + flags: flags, + rawRef: record.ReferenceID + } + }); + resolve(dids); + }) + .catch(error => { + console.error("Error fetching DIDs:", error); + reject(error); + }); + }); +} + +async function getAllDIDs() { + // Use the same BVS API endpoint, but just get all the results, not just premium ones. + return new Promise((resolve, reject) => { + fetch(`https://portal.bulkvs.com/api/v1.0/tnRecord?Status=Active&Trunk%20Group=${process.env.TRUNK_GROUP}`, { + headers: { + "Authorization": `Bearer ${process.env.BVS_TOKEN}`, + } + }) + .then(res => { + if (!res.ok) { + throw new Error(`Error fetching DIDs: ${res.status} ${res.statusText}`); + } + return res.json(); + }) + .then(data => { + const dids = data.map(record => record.TN); + resolve(dids); + }) + .catch(error => { + console.error("Error fetching DIDs:", error); + reject(error); + }); + }); +} + +async function getAccountInfo() { + const res = await fetch("https://portal.bulkvs.com/api/v1.0/accountDetail", { + headers: { + "Authorization": `Bearer ${process.env.BVS_TOKEN}`, + } + }); + + if (!res.ok) { + throw new Error(`BVS_TOKEN is invalid: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +async function searchDIDs(query) { + // Query will either be NPA, or NPANXX. Validate with regex, then use the API to find some. + // API Example: GET https://portal.bulkvs.com/api/v1.0/orderTn?Npa=310&Nxx=906&Lca=true&Limit=100 + query = query.replace(/\D/g, ""); // Remove all non-digit characters + // Validate query w regex + if (!/^\d{3}$/.test(query) && !/^\d{6}$/.test(query)) { + throw new Error("Query must be either NPA (3 digits) or NPANXX (6 digits)"); + } + + const res = await fetch(`https://portal.bulkvs.com/api/v1.0/orderTn?Npa=${query.slice(0, 3)}&Nxx=${query.slice(3, 6)}&Lca=true&Limit=100`, { + headers: { + "Authorization": `Bearer ${process.env.BVS_TOKEN}`, + } + }); + + if (!res.ok) { + throw new Error(`Error searching DIDs: ${res.status} ${res.statusText}`); + } + + return res.json(); +} + +async function searchPurchasableDIDs(query) { + // Use searchDIDs, then filter down to 6 random from the results where the Nrc value == "0.05" and Mrc == "0.06" + const dids = await searchDIDs(query).catch(error => { + console.error("Error searching DIDs:", error); + return []; + }); + // console.log(dids) + const purchasableDIDs = dids.filter(did => did.Nrc <= "0.50" && did.Mrc === "0.06"); + // console.log(purchasableDIDs) + // Shuffle the purchasableDIDs array, then take the first 6 + let shuffled = [...purchasableDIDs]; + for (let i = 0; i < 5; i++) { + shuffled = shuffled.sort(() => 0.5 - Math.random()); + } + // shuffle more + + return shuffled.slice(0, 6); +} + +async function purchaseDID(did, userId) { + // POST /orderTn with body: + /* + { + "TN": "did to purchase", + "Lidb": "LITENET", + "Portout Pin": `${random 12 digit number}`, + "ReferenceID": `;;${userId}`, + "Trunk Group": `${process.env.TRUNK_GROUP}`, + "Sms": false, + "Mms": false, + "Webhook": "Default" +} + */ + let portoutPin = ""; + const digits = "0123456789"; + for (let i = 0; i < 12; i++) { + portoutPin += digits[Math.floor(Math.random() * digits.length)]; + } + const res = await fetch("https://portal.bulkvs.com/api/v1.0/orderTn", { + method: "POST", + headers: { + "Authorization": `Bearer ${process.env.BVS_TOKEN}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + "TN": did, + "Lidb": "LITENET", + "Portout Pin": portoutPin, + "ReferenceID": `;;${userId}`, + "Trunk Group": `${process.env.TRUNK_GROUP}`, + "Sms": false, + "Mms": false, + "Webhook": "Default" + }) + }); + // If response is 404, check body.json().Description, return the json body. + if (res.status === 404) { + const errorData = await res.json(); + throw new Error(`Error purchasing DID: ${res.status} ${res.statusText} - ${errorData.Description}`); + } + if (!res.ok) { + throw new Error(`Error purchasing DID: ${res.status} ${res.statusText}`); + } + return res.json(); +} + +function formatPhoneNumber(input) { + if (/^\d{10}$/.test(input)) { + input = `1${input}`; + } + // Regex to make sure it's a valid 11 digit NA phone number. + if (!/^\d{11}$/.test(input)) { + throw new Error("Phone number must be 10 or 11 digits, including country code (1 for US/Canada)."); + } + // Format to human readable format: +1 (310) 906-1234 + return `+${input[0]} (${input.slice(1, 4)}) ${input.slice(4, 7)}-${input.slice(7)}`; +} + +module.exports = { + getAllDIDs, + getAccountInfo, + getPremiumDIDs, + searchDIDs, + searchPurchasableDIDs, + purchaseDID, + formatPhoneNumber +} \ No newline at end of file diff --git a/bvsTest.js b/bvsTest.js new file mode 100644 index 0000000..2ace6a7 --- /dev/null +++ b/bvsTest.js @@ -0,0 +1,40 @@ +require("dotenv").config({quiet: true}); +const bvs = require("./bvs"); + +// bvs.getAccountInfo().then(accountInfo => { +// console.log(`Validated BulkVS Token. Account Contact is ${accountInfo["Main Contact"].Name}`) +// }).catch(error => { +// console.error("Error validating BVS_TOKEN:", error); +// process.exit(1); +// }) + +// bvs.getAllDIDs().then(dids => { +// console.log(`Fetched ${dids.length} DIDs from BulkVS.`); +// console.log(dids); +// }).catch(error => { +// console.error("Error fetching DIDs:", error); +// }); + +bvs.getPremiumDIDs().then(dids => { + console.log(`Fetched ${dids.length} premium DIDs from BulkVS.`); + console.log(dids); +}).catch(error => { + console.error("Error fetching premium DIDs:", error); +}); + +bvs.searchPurchasableDIDs("910").then(dids => { + console.log(`Fetched ${dids.length} DIDs from BulkVS with user ID 610548.`); + console.log(dids.map(did => did.TN)) + // console.log(dids) + // Test purchase the first one, if there is one + if (dids.length > 0) { + // bvs.purchaseDID(dids[0].TN, 8949849456165).then((data) => { + // console.log(`Successfully purchased DID ${dids[0].TN}`); + // console.log(data) + // }).catch(error => { + // console.error(`Error purchasing DID ${dids[0].TN}:`, error); + // }); + } +}).catch(error => { + console.error("Error fetching DIDs with user ID 610548:", error); +}); \ No newline at end of file diff --git a/commands.js b/commands.js index a71ede6..f7baa67 100644 --- a/commands.js +++ b/commands.js @@ -2,15 +2,37 @@ const Discord = require('discord.js'); module.exports = [ { - name: 'checkmember', - description: 'Check if a member is boosting the guild.', - options: [ - { - name: 'user', - description: 'The user to check. If not provided, checks the user who ran the command.', - type: Discord.ApplicationCommandOptionType.User, // USER type - required: false, - } - ] + name: 'mynumber', + description: 'Get your associated DID, if you have one.', }, + { + name: 'searchnumbers', + description: 'Search for DIDs, and purchase one if you have an active server boost.', + options: [ + { + name: 'area_code', + description: 'The area code to search for (e.g. 610).', + type: Discord.ApplicationCommandOptionType.Number, + required: true + }, + { + name: 'office_code', + description: 'The office code to search for (e.g. 548).', + type: Discord.ApplicationCommandOptionType.Number, + required: false + } + ] + }, + { + name: 'getnumber', + description: 'Purchase a DID from the latest search results of /searchnumbers.', + options: [ + { + name: 'choice', + description: 'The choice of DID to purchase from the latest search results.', + type: Discord.ApplicationCommandOptionType.Number, + required: true + } + ] + } ] \ No newline at end of file diff --git a/index.js b/index.js index 1f2ab7b..3c51b14 100644 --- a/index.js +++ b/index.js @@ -7,49 +7,12 @@ const client = new Discord.Client({ ] }) +const bvs = require("./bvs"); + + const { REST, Routes } = require("discord.js"); const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN); -async function getAllDIDs() { - return new Promise((resolve, reject) => { - fetch(`https://portal.bulkvs.com/api/v1.0/tnRecord?Status=Active&Trunk%20Group=${process.env.TRUNK_GROUP}`, { - headers: { - "Authorization": `Bearer ${process.env.BVS_TOKEN}`, - } - }) - .then(res => { - if (!res.ok) { - throw new Error(`Error fetching DIDs: ${res.status} ${res.statusText}`); - } - return res.json(); - }) - .then(data => { - data.forEach(record => { - console.log(record.ReferenceID); - }); - // data is array of objects. If object.ReferenceID.split(";;")[0] is premDID, then [1] is the user id, and anything after is a note - const dids = data.filter(record => record.ReferenceID && record.ReferenceID.startsWith(";;")).map(record => { - // Get all flags (just keep looking at the next split item until its not a valid flag, after that, it's all a custom note, most likely to be used fo) - return { - did: record.TN, - userId: record.ReferenceID.split(";;")[1], - } - }); - resolve(dids); - }) - .catch(error => { - console.error("Error fetching DIDs:", error); - reject(error); - }); - }); -} - -getAllDIDs().then(dids => { - console.log(`Fetched ${dids.length} DIDs from BulkVS.`); - console.log(dids) -}).catch(error => { - console.error("Error fetching DIDs:", error); -}); client.on("clientReady", () => { console.log(`Logged in as ${client.user.tag}!`); @@ -69,28 +32,6 @@ client.on("clientReady", () => { rest.put(Routes.applicationGuildCommands(client.user.id, process.env.HOME_GUILD), { body: commands }) .then(() => console.log("Successfully registered application commands.")) .catch(console.error); - - // Check that we have a valid BVS_TOKEN with https://portal.bulkvs.com/api/v1.0/accountInfo (Bearer token in Authorization header) - if (!process.env.BVS_TOKEN) { - console.error("BVS_TOKEN environment variable is not set. Please set it to a valid token from https://portal.bulkvs.com/api/v1.0/accountInfo (Bearer token in Authorization header)."); - process.exit(1); - } - fetch("https://portal.bulkvs.com/api/v1.0/accountDetail", { - headers: { - "Authorization": `Bearer ${process.env.BVS_TOKEN}`, - } - }).then(res => { - if (!res.ok) { - throw new Error(`BVS_TOKEN is invalid: ${res.status} ${res.statusText}`); - } - // Get raw text - return res.json(); - }).then(data => { - console.log(`Validated BulkVS Token. Account Contact is ${data["Main Contact"].Name}`) - }).catch(error => { - console.error("Error validating BVS_TOKEN:", error); - process.exit(1); - }); }); client.on('interactionCreate', async interaction => { @@ -98,7 +39,7 @@ client.on('interactionCreate', async interaction => { if (interaction.isChatInputCommand()) { try { const handler = require(`./interactions/chatCommand/${interaction.commandName}.js`); - await handler(interaction, client); + await handler(interaction, client, bvs); } catch (error) { console.error(`Error handling interaction ${interaction.id}:`, error); } diff --git a/interactions/chatCommand/getnumber.js b/interactions/chatCommand/getnumber.js new file mode 100644 index 0000000..89dfaea --- /dev/null +++ b/interactions/chatCommand/getnumber.js @@ -0,0 +1,21 @@ +module.exports = (interaction, client, bvs) => { + const choice = interaction.options.getNumber('choice'); + if (!choice) { + return interaction.reply({ content: `You must provide a choice from the latest run of \`/searchnumbers\`. Use \`/searchnumbers\` to see available DIDs and their corresponding choices.`, ephemeral: true }); + } + + if (!global.tempPurchasableDIDs || !global.tempPurchasableDIDs[interaction.user.id]) { + return interaction.reply({ content: `You must provide a choice from the latest run of \`/searchnumbers\`. Use \`/searchnumbers\` to see available DIDs and their corresponding choices.`, ephemeral: true }); + } + + const dids = global.tempPurchasableDIDs[interaction.user.id]; + const index = parseInt(choice) - 1; + if (isNaN(index) || index < 0 || index >= dids.length) { + return interaction.reply({ content: `Invalid choice. You must provide a choice from the latest run of \`/searchnumbers\`. Use \`/searchnumbers\` to see available DIDs and their corresponding choices.`, ephemeral: true }); + } + + const didToPurchase = dids[index]; + return interaction.reply({ content: `Attempting to purchase DID \`${bvs.formatPhoneNumber(didToPurchase)}\`...`, ephemeral: true }).then(() => { + // wait, we are in dev phase, dont buy lol + }); +} \ No newline at end of file diff --git a/interactions/chatCommand/mynumber.js b/interactions/chatCommand/mynumber.js new file mode 100644 index 0000000..e963aa3 --- /dev/null +++ b/interactions/chatCommand/mynumber.js @@ -0,0 +1,23 @@ +module.exports = (interaction, client, bvs) => { + const member = interaction.guild.members.cache.get(interaction.user.id); + + if (!member) { + interaction.reply({ content: `Could not find member with ID ${user.id}.`, ephemeral: true }); + return; + } + bvs.getPremiumDIDs().then(dids => { + const userDIDs = dids.filter(did => did.userId === interaction.user.id); + if (userDIDs.length === 0) { + if (member.premiumSince) { + interaction.reply({ content: `You don't have a DID associated with your account. But with your currently active server boost, you can get one with \`/getnumber\`!`, ephemeral: true }); + } else { + interaction.reply({ content: `You don't have a DID associated with your account. Boost the server to request one!`, ephemeral: true }); + } + } else { + interaction.reply({ content: `Your DID is \`${bvs.formatPhoneNumber(userDIDs[0].did)}\``, ephemeral: true }); + } + }).catch(error => { + console.error("Error fetching premium DIDs:", error); + interaction.reply({ content: `There was an error fetching your DID. Please try again later.`, ephemeral: true }); + }); +} \ No newline at end of file diff --git a/interactions/chatCommand/searchnumbers.js b/interactions/chatCommand/searchnumbers.js new file mode 100644 index 0000000..2c9949e --- /dev/null +++ b/interactions/chatCommand/searchnumbers.js @@ -0,0 +1,25 @@ +module.exports = (interaction, client, bvs) => { + const areaCode = interaction.options.getNumber('area_code'); + const officeCode = interaction.options.getNumber('office_code'); + // Validate input + if (!/^\d{3}$/.test(areaCode)) { + return interaction.reply({ content: `Area code must be 3 digits.`, ephemeral: true }); + } + if (officeCode && !/^\d{3}$/.test(officeCode)) { + return interaction.reply({ content: `Office code must be 3 digits.`, ephemeral: true }); + } + bvs.searchPurchasableDIDs(areaCode + (officeCode || "")).then(dids => { + if (dids.length === 0) { + return interaction.reply({ content: `No results for search query. Try again.`, ephemeral: true }); + } + console.log(dids) + interaction.reply({ content: `Found the following DIDs\n\`\`\`\n${dids.map((did, index) => `${index + 1}. ${bvs.formatPhoneNumber(did.TN)}`).join("\n")}\n\`\`\`\nUse \`/getnumber \` to purchase one of these DIDs! (Choice is one of the numbers listed above by index, not phone number)`, ephemeral: true }); + // Store the DIDs in a global temp variable with the user id so the user can purchase by 1, 2, 3, etc. in the /getnumber command. This is a bit janky but it works for now. + global.tempPurchasableDIDs = global.tempPurchasableDIDs || {}; + global.tempPurchasableDIDs[interaction.user.id] = dids.map(did => did.TN); + console.log(global.tempPurchasableDIDs) + }).catch(error => { + console.error("Error searching DIDs:", error); + interaction.reply({ content: `There was an error searching for DIDs. Please try again later.`, ephemeral: true }); + }); +} \ No newline at end of file