├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hetzner-notifier 2 | ================ 3 | 4 | Automatically send email notifications when Hetzner's server auctions drop below 5 | a certain price threshold. 6 | 7 | Usage 8 | ===== 9 | 10 | ``` 11 | npm install 12 | node index.js --email_to your@email.com --smtp_host smtp.server.com --smtp_accept_unauthorized 13 | ``` 14 | 15 | Parameters 16 | ---------- 17 | 18 | *Email parameters:* 19 | 20 | - `smtp_host` - Required: The SMTP server to use. 21 | - `smtp_port` - Optional: The SMTP port to connect to (default: 25). 22 | - `smtp_username` - Optional: The SMTP username. 23 | - `smtp_password` - Optional: The SMTP password. 24 | - `smtp_accept_unauthorized` - Optional: If this flag is passed in, the script 25 | will accept connection to a SMTP server with a self-signed TLS key. 26 | - `email_to` - Required: The email address to email notifications to. 27 | - `email_from` - Optional: The email address to email notifications from 28 | (default: `hetzner@me.com`). 29 | 30 | *Hetzner specific parameters:* 31 | - `country` - Optional: The country you're in. 32 | Hetzner charges different prices based on this information (default: US). 33 | - `threshold` - Optional: The price threshold (in euro) below which 34 | notifications are sent (default: 30). 35 | - `ram` - Optional: Minimum RAM (in GB). 36 | - `hdnr` - Optional: Minimum number of HDD's. 37 | - `hdsize` - Optional: Minimum HDD size (in GB). 38 | - `text` - Optional: Miscellaneous text to search (e.g. SSD) 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var $ = require("cheerio"); 2 | var nodemailer = require("nodemailer"); 3 | var rc = require("rc"); 4 | var request = require("request"); 5 | 6 | var config = rc("hetzner-notifier", { 7 | country: "US", 8 | ram: "", 9 | hdnr: "", 10 | hdsize: "", 11 | text: "", 12 | smtp_host: "", 13 | smtp_port: 25, 14 | smtp_username: "", 15 | smtp_password: "", 16 | smtp_accept_unauthorized: false, 17 | email_from: "hetzner@me.com", 18 | email_to: "", 19 | threshold: 30 20 | }); 21 | 22 | var url = "https://robot.your-server.de/order/market"; 23 | var countryUrl = url + "/country/" + config.country; 24 | 25 | // We don't set the maxprice here, since we want to get the next lowest price when nothing under 26 | // the threshold is found. 27 | var formData = { 28 | ram: config.ram, 29 | hdnr: config.hdnr, 30 | hdsize: config.hdsize, 31 | maxprice: "", 32 | text: config.text, 33 | limit: "100" 34 | }; 35 | 36 | if (!config.smtp_host) { 37 | console.error("Please specity SMTP host"); 38 | process.exit(1); 39 | } 40 | 41 | if (!config.email_to) { 42 | console.error("Please specify the email address to send notifications to"); 43 | process.exit(1); 44 | } 45 | 46 | var transporter = nodemailer.createTransport({ 47 | host: config.smtp_host, 48 | port: config.smtp_port, 49 | auth: { 50 | user: config.smtp_username, 51 | pass: config.smtp_password 52 | }, 53 | tls: { 54 | rejectUnauthorized: !config.smtp_accept_unauthorized 55 | } 56 | }); 57 | 58 | var jar = request.jar(); 59 | request = request.defaults({jar: jar}); 60 | 61 | // We go to the country page first, because the pricing is dependent on the country the visitor 62 | // chooses. By going to the page, a cookie is set so that subsequent search gets the correct prices 63 | request.get(countryUrl, function () { 64 | request.post(url, {form: formData}, function (error, response, body) { 65 | if (error || response.statusCode !== 200) { 66 | console.error("Error getting Hetzner webpage: " + body); 67 | process.exit(2); 68 | } 69 | var lowestPrice = $(body).find(".order_price").eq(2).text().split(" ")[1]; 70 | if (Number(lowestPrice) <= config.threshold) { 71 | var mailOptions = { 72 | from: config.email_from, // sender address 73 | to: config.email_to, // list of receivers 74 | subject: "Hetzner Server deal found", // Subject line 75 | text: "Hetzner server dropped under " + config.threshold, // plaintext body 76 | }; 77 | transporter.sendMail(mailOptions, function(error, info){ 78 | if (error) { 79 | console.error("Error sending email notification: " + error); 80 | process.exit(3); 81 | } else { 82 | console.log('Email notification sent: ' + info.response); 83 | } 84 | }); 85 | } else { 86 | console.log("Nothing found. The cheapest deal is " + lowestPrice); 87 | } 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hetzner-notifier", 3 | "version": "1.0.1", 4 | "description": "Notify Hetzner's server sniping deals under certain price threshold", 5 | "main": "index.js", 6 | "keywords": [ 7 | "hetzner" 8 | ], 9 | "dependencies": { 10 | "request": "2.44.0", 11 | "cheerio": "0.17.0", 12 | "nodemailer": "1.3.0", 13 | "rc": "0.5.1" 14 | }, 15 | "author": "Dang Mai ", 16 | "license": "ISC" 17 | } 18 | --------------------------------------------------------------------------------