├── codes ├── purchases.json └── refunds.json ├── .gitignore ├── package.json ├── .env.example ├── README.md └── index.js /codes/purchases.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /codes/refunds.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .idea -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appsumo-refunds-and-new-purchases", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.1.3", 13 | "dotenv": "^16.0.3", 14 | "lodash": "^4.17.21", 15 | "mailgun-js": "^0.22.0", 16 | "node-schedule": "^2.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MAILGUN_KEY=d4sdfsdfsdff0dcea76adfac69e29-3424234-3453535 2 | MAILGUN_DOMAIN=usp.ai 3 | FROM_EMAIL="Nevo David " 4 | REVOKE_URL=https://appsumo.com/partners/codes/revoked/{id}/ 5 | REDEEM_URL=https://appsumo.com/partners/codes/redeemed/{id}/ 6 | SEND_TO_EMAILS=nevo@linvo.io,yuvalsuede@gmail.com 7 | # Just open one time one of your revoked/redeemed csv and copy the entire cookie from the request 8 | APPSUMO_COOKIES="" 9 | WEBSITE_REFUND_URL=https://yourdomain.com/revoke/${code}?api=protection -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | logo 4 | 5 |

6 |

Welcome to AppSumo new refunds and new purchases bot

7 |

8 | 9 | License: MIT License 10 | 11 |

12 | 13 | AppSumo is a great platform, but they don't let us know when there is a new refund or a new purchase. 14 | 15 | This small bot check your AppSumo partners page every minute for new refunds and purchases. 16 | 17 | - On a new purchase, we send an email 18 | 19 | - On a new refund we send an email and an http request for the refunds endpoint 20 | 21 | ## 📝 License 22 | 23 | This project is [MIT License](https://opensource.org/licenses/MIT) licensed. 24 | 25 | *** 26 | We accept contribution with great love! Show your interest! Contribute! 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const mailgun = require('mailgun-js'); 3 | const {readFileSync, writeFileSync} = require('fs'); 4 | const {difference} = require('lodash'); 5 | const schedule = require('node-schedule'); 6 | require('dotenv').config() 7 | 8 | const mg = mailgun({ 9 | apiKey: node.env.MAILGUN_KEY, 10 | domain: node.env.MAILGUN_DOMAIN, 11 | }); 12 | 13 | const oldSend = ( 14 | address, 15 | subject, 16 | template, 17 | ) => { 18 | return mg.messages().send({ 19 | from: process.env.FROM_EMAIL, 20 | to: address.join(', '), 21 | subject: subject, 22 | html: template 23 | }); 24 | } 25 | 26 | const loadCodes = (name) => { 27 | const list = readFileSync(`./codes/${name}.json`).toString(); 28 | return JSON.parse(list); 29 | } 30 | 31 | const saveCodes = (name, codes) => { 32 | writeFileSync(`./codes/${name}.json`, JSON.stringify(codes)); 33 | } 34 | 35 | const refunds = async () => { 36 | const {data} = await axios.get(node.env.REVOKE_URL, { 37 | responseType: 'blob', 38 | headers: { 39 | cookie: node.env.APPSUMO_COOKIES 40 | } 41 | }); 42 | 43 | const codes = data.split('\n').map(p => p.trim()).filter(f => f); 44 | const oldCodes = loadCodes('refunds'); 45 | const newCodesToRefund = difference(codes, oldCodes); 46 | if (newCodesToRefund.length) { 47 | oldSend([node.env.SEND_TO_EMAILS.split(',')], 'New Refunds', `There are ${newCodesToRefund.length} new refunds: ${newCodesToRefund.join(', ')}`) 48 | saveCodes('refunds', codes); 49 | return Promise.all(newCodesToRefund.map((code) => { 50 | return axios.get(node.env.WEBSITE_REFUND_URL.replace(':code', code)) 51 | })); 52 | } 53 | } 54 | 55 | const newPurchases = async () => { 56 | const {data} = await axios.get(node.env.REDEEM_URL, { 57 | responseType: 'blob', 58 | headers: { 59 | cookie: node.env.APPSUMO_COOKIES 60 | } 61 | }); 62 | 63 | const codes = data.split('\n').map(p => p.trim()).filter(f => f); 64 | const oldCodes = loadCodes('purchases'); 65 | const refunds = loadCodes('refunds'); 66 | const newCodesToRefund = difference(codes, oldCodes); 67 | if (newCodesToRefund.length) { 68 | oldSend([node.env.SEND_TO_EMAILS.split(',')], 'New Purchases', `There are ${codes.length - refunds.length} in the system >> ${newCodesToRefund.length} new purchases`); 69 | saveCodes('purchases', codes); 70 | } 71 | } 72 | 73 | schedule.scheduleJob('* * * * *', async () => { 74 | return Promise.all([ 75 | newPurchases(), 76 | refunds() 77 | ]); 78 | }); 79 | --------------------------------------------------------------------------------