UNPKG

najdisi-sms-api

Version:

Send 40 free SMS per day in Slovenia with Najdi.si.

347 lines (305 loc) 10.9 kB
"use strict"; const axios = require("axios"); const axiosCookieJarSupport = require("@3846masa/axios-cookiejar-support").default; const cheerio = require("cheerio"); const querystring = require("querystring"); const tough = require("tough-cookie"); const Joi = require("./libs/joi-ext"); /** * Create a new axios instance that is setup to send requests to najdi.si. */ function createAxios() { let cookieJar = new tough.CookieJar(); let instance = axios.create({ baseURL: "https://www.najdi.si", timeout: 30000, jar: cookieJar, // tough.CookieJar or boolean withCredentials: true, // If true, send cookies stored in jar headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/63.0.3239.84 Chrome/63.0.3239.84 Safari/537.36", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", // X-Requested-With header is used to prevent CORS attacks. // souce: https://stackoverflow.com/questions/17478731/whats-the-point-of-the-x-requested-with-header "X-Requested-With": "XMLHttpRequest", }, }); instance.cookieJar = cookieJar; // Add cookie jar support to axios. axiosCookieJarSupport(instance); return instance; } function pushToObj(obj, name, value) { if (name in obj) { if (obj[name].constructor !== Array) obj[name] = [obj[name]]; obj[name].push(value); } else obj[name] = value; } /** * Extract data that would get submitted with the form. * This function is compatible with vanilla js. * * @param {jQuery|cheerio} $ * @param {string} formId */ function formToObj($, formId) { var form = $(`form#${formId}`); if (form.length < 1) throw new Error(`${formId} form not found.`); var dataObj = {}; var optionsObj = {}; var elements = form.find("input,select,textarea"); elements.each(function() { var element = $(this); var tagName = element[0].tagName; var type = element.attr("type"); var name = element.attr("name"); var value = ""; // Handle each type of element. // NOTE: for textarea do nothing. if (tagName === "input") { if (type === "submit") return; if ((type !== "hidden") || (element.attr("id") == null)) { value = element.attr("value") || ""; } } else if (tagName === "select") { // Extract select options. var options = element.find("option"); var optionValues = []; options.each(function() { var option = $(this); var optionValue = option.attr("value") || ""; optionValues.push(optionValue); if (option.attr("selected") != null) value = optionValue; }); pushToObj(optionsObj, name, optionValues); } pushToObj(dataObj, name, value); }); return { form: form, data: dataObj, options: optionsObj, }; } function extractStatus($) { // Plan // - is logged in flag // - name and surname of the user // - current and max sms count // - is sender set let status = { isLoggedIn: false, name: null, smsCount: null, maxSmsCount: null, isSenderSet: null, phoneNumberSender: null, }; // Find "Prijava" button. let loginLink = $("div#nav1 a#loginPovezava"); if (loginLink.length > 0) { // Not logged in. return status; } // Logged in => get name and surname. status.isLoggedIn = true; let nameLink = $("div#nav1 div.pull-right ul.topnav > li > a"); if (nameLink.length > 0) { status.name = nameLink.text(); } // Check if SMS form exists. let smsForm = $("form#smsForm"); if (smsForm.length > 0) { status.isSenderSet = false; // Get SMS count and max count. let smsCountLabel = smsForm.find("div#formZone ul.reciever div.smsno > strong"); if (smsCountLabel.length > 0) { let smsCountText = smsCountLabel.text().trim(); if (/^\d+\s*\/\s*\d+$/.test(smsCountText)) { let counts = smsCountText.split("/").map(n => parseInt(n.trim())); if (counts.length >= 2) { status.smsCount = counts[0]; status.maxSmsCount = counts[1]; } } } // Get sender info. let senderInfoLabel = smsForm.find("div#formZone div#firstName-container span.senderinfo"); if (senderInfoLabel.length > 0) { let senderInfoText = $(senderInfoLabel[0]).text().trim(); if (/^\d+\s*\/\s*\d+\s*\-\s*\d+$/.test(senderInfoText)) { status.isSenderSet = true; status.phoneNumberSender = senderInfoText; } } } return status; } /** * Perform login preocedure to acquire cookies. * * @param {axios} axiosInstance * @param {string} username * @param {string} password * @param {boolean} [rememberMe=false] */ async function loginHelper(axiosInstance, username, password, rememberMe = false) { // Load login form. let response = await axiosInstance.get("/prijava"); let $ = cheerio.load(response.data); let {form, data} = formToObj($, "jsecLoginForm"); // Validate the login form. data = Joi.validate(data, Joi.object().keys({ jsecLogin: Joi.string().allow("").required(), jsecPassword: Joi.string().allow("").required(), jsecRememberMe: Joi.string().allow("").required(), })); // Set parameters. data.jsecLogin = username; data.jsecPassword = password; if (rememberMe) data.jsecRememberMe = "on"; else delete data.jsecRememberMe; // Validate form action. let formAttrs = Joi.validate(form[0].attribs, Joi.object().keys({ action: Joi.string().required(), method: Joi.string().valid("post").required(), })); // Login. response = await axiosInstance.post(formAttrs.action, querystring.stringify(data)); // Extract status and return. let status = extractStatus(cheerio.load(response.data)); return status; } async function sendSmsHelper(axiosInstance, areaCodeRecipient, phoneNumberRecipient, text) { // Load send sms form. let response = await axiosInstance.get("/najdi/sms"); let $ = cheerio.load(response.data); // Check if sender is set. let status = extractStatus($); if (status.isLoggedIn !== true) throw new Error("Need to log in."); if (status.isSenderSet !== true) throw new Error("Sender phone number not set and verified."); if (!(status.maxSmsCount > 0)) throw new Error("Could not get maxSmsCount from status."); if (status.smsCount >= status.maxSmsCount) throw new Error(`Daily limit of ${status.maxSmsCount} SMS reached.`); let {form, data, options} = formToObj($, "smsForm"); // Validate the send sms form. options = Joi.validate(options, Joi.object().keys({ areaCodeRecipient: Joi.array().items(Joi.string().allow("")).required(), })); options.areaCodeRecipient = options.areaCodeRecipient.map(ac => ac.trim()).filter(ac => (ac.length > 0)); data = Joi.validate(data, Joi.object().keys({ areaCodeRecipient: Joi.string().allow("").required(), phoneNumberRecipient: Joi.string().allow("").required(), text: Joi.string().allow("").required(), })); // Validate & set parameters. let areaCodeSet = new Set(options.areaCodeRecipient); if (!areaCodeSet.has(areaCodeRecipient)) { throw new Error(`Ivalid area code. Pick one of ${options.areaCodeRecipient.join(", ")}.`) } data.areaCodeRecipient = areaCodeRecipient; data.phoneNumberRecipient = phoneNumberRecipient; data.text = text; data["t:submit"] = `["send","send"]`; data["t:zoneid"] = `smsZone`; // Validate form action. let formAttrs = Joi.validate(form[0].attribs, Joi.object().keys({ action: Joi.string().required(), method: Joi.string().valid("post").required(), })); // Send sms. await axiosInstance.post(formAttrs.action, querystring.stringify(data)); } class NajdisiSmsApi { constructor() { this.isLoggedIn = false; this.axiosInstance = createAxios(); } /** * Log in. * * @param {string} username Najdi.si account username. * @param {string} password Najdi.si account password. */ async login(username, password) { // Validate parameters. let p = Joi.validate({username, password}, Joi.object().keys({ username: Joi.string().allow("").required(), password: Joi.string().allow("").required(), })); this.isLoggedIn = false; let status = await loginHelper(this.axiosInstance, p.username, p.password); if (!status.isLoggedIn) throw new Error("Login failed."); this.isLoggedIn = true; } /** * Get status. */ async getStatus() { // Load send sms form. let response = await this.axiosInstance.get("/najdi/sms"); // Extract status and return. let status = extractStatus(cheerio.load(response.data)); return status; } /** * Send sms. * * @param {string} areaCodeRecipient Must have length of 3. Examples: "030", "070", "041", ... * @param {string} phoneNumberRecipient Must have length of 6. Example: "123456", ... * @param {string} smsText SMS text that you want to send. Max 160 characters. */ async sendSms(areaCodeRecipient, phoneNumberRecipient, smsText) { // Validate parameters. let p = Joi.validate({areaCodeRecipient, phoneNumberRecipient, smsText}, Joi.object().keys({ areaCodeRecipient: Joi.phoneNumber(3).required(), phoneNumberRecipient: Joi.phoneNumber(6).required(), smsText: Joi.string().max(160).allow("").required(), })); if (!this.isLoggedIn) throw new Error("Need to log in first."); await sendSmsHelper(this.axiosInstance, p.areaCodeRecipient, p.phoneNumberRecipient, p.smsText); } /** * Get status without creating an API instance. * * @param {string} username Najdi.si account username. * @param {string} password Najdi.si account password. */ static async getStatusOnce(username, password) { // Validate parameters. let p = Joi.validate({username, password}, Joi.object().keys({ username: Joi.string().allow("").required(), password: Joi.string().allow("").required(), })); let api = new NajdisiSmsApi(); await api.login(p.username, p.password); let status = await api.getStatus(); return status; } /** * Send SMS without creating an API instance. * * @param {string} username Najdi.si account username. * @param {string} password Najdi.si account password. * @param {string} areaCodeRecipient Must have length of 3. Examples: "030", "070", "041", ... * @param {string} phoneNumberRecipient Must have length of 6. Example: "123456", ... * @param {string} smsText SMS text that you want to send. Max 160 characters. */ static async sendSmsOnce(username, password, areaCodeRecipient, phoneNumberRecipient, smsText) { // Validate parameters. let p = Joi.validate({username, password, areaCodeRecipient, phoneNumberRecipient, smsText}, Joi.object().keys({ username: Joi.string().allow("").required(), password: Joi.string().allow("").required(), areaCodeRecipient: Joi.phoneNumber(3).required(), phoneNumberRecipient: Joi.phoneNumber(6).required(), smsText: Joi.string().max(160).allow("").required(), })); let api = new NajdisiSmsApi(); await api.login(p.username, p.password); let status = await api.sendSms(p.areaCodeRecipient, p.phoneNumberRecipient, p.smsText); return status; } } /** * Exported NajdisiSmsApi class. * @type {class} */ module.exports = NajdisiSmsApi;