├── .gitignore ├── vercel.json ├── package.json └── src ├── helpers └── twitter.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.xlsx 3 | .env -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "(.*)", 12 | "dest": "src/index.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-campaign-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "dev": "nodemon src/index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.24.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^10.0.0", 18 | "express": "^4.17.2", 19 | "formidable": "^3.2.1", 20 | "nodemon": "^2.0.15", 21 | "xlsx": "^0.17.5" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/twitter.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const wait = (ms) => { 4 | return new Promise((resolve) => { 5 | setTimeout(resolve, ms); 6 | }); 7 | }; 8 | 9 | export const getUsername = (users) => { 10 | const LINK_REGEX = /(?<=twitter.com\/).*(?=\?)/; 11 | const ALT_REGEX = /(?<=twitter.com\/).*/; 12 | const STATUS_REGEX = /\/status.*/; 13 | const USERNAME_REGEX = /^[A-Za-z0-9_]{1,15}$/; 14 | 15 | return users.map((user) => { 16 | var username; 17 | if (user.startsWith("@")) { 18 | username = user; 19 | } else if (LINK_REGEX.test(user)) { 20 | username = LINK_REGEX.exec(user)[0]; 21 | } else if (ALT_REGEX.test(user)) { 22 | username = ALT_REGEX.exec(user)[0]; 23 | } 24 | if (!username) { 25 | return ""; 26 | } 27 | username = username.replace("@", ""); 28 | username = username.replace(STATUS_REGEX, ""); 29 | return USERNAME_REGEX.test(username) ? username : ""; 30 | }); 31 | }; 32 | 33 | export const getFollowers = async (usernames) => { 34 | var usersData = []; 35 | const headers = { 36 | Authorization: `Bearer ${process.env.TOKEN}`, 37 | }; 38 | const url = "https://api.twitter.com/2/users/by"; 39 | for (let index = 0; index < usernames.length; index += 100) { 40 | const params = { 41 | usernames: usernames.slice(index, index + 100).join(), 42 | "user.fields": "public_metrics", 43 | }; 44 | const response = await axios.get(url, { 45 | headers, 46 | params, 47 | }); 48 | usersData = usersData.concat(response.data.data); 49 | await wait(1000); 50 | } 51 | return usersData; 52 | }; 53 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import formidable from "formidable"; 4 | import xlsx from "xlsx"; 5 | import "dotenv/config"; 6 | import { getUsername, getFollowers } from "./helpers/twitter.js"; 7 | 8 | const PORT = 3000; 9 | const app = express(); 10 | app.use(cors()); 11 | 12 | app.get("/", (req, res) => { 13 | return res.status(200).send({ hello: "world" }); 14 | }); 15 | 16 | app.post("/process", async (req, res, next) => { 17 | var form = formidable(); 18 | res.setHeader("Content-Disposition", "attachment;filename=output.xlsx"); 19 | res.setHeader("Content-type", "application/octet-stream"); 20 | 21 | form.parse(req, async (err, fields, files) => { 22 | if (err) { 23 | next(err); 24 | return; 25 | } 26 | const xlsxFile = files[Object.keys(files)[0]]; 27 | if (!xlsxFile) { 28 | return res.status(400).send({ error: "No file uploaded" }); 29 | } 30 | const wb = xlsx.readFile(xlsxFile[0].filepath); 31 | const worksheetName = wb.SheetNames[0]; 32 | var jsonData = xlsx.utils.sheet_to_json(wb.Sheets[worksheetName]); 33 | const twitterLinks = jsonData.map((data) => data.twitter); 34 | const usernames = getUsername(twitterLinks); 35 | jsonData = jsonData.filter((_row, idx) => { 36 | return usernames[idx]; 37 | }); 38 | const followers = await getFollowers( 39 | usernames.filter((username) => username) 40 | ); 41 | var idx = 0; 42 | jsonData = jsonData 43 | .filter((row) => { 44 | const deleteRow = row.twitter 45 | .toLowerCase() 46 | .includes(followers[idx].username.toLowerCase()); 47 | 48 | if (deleteRow) idx++; 49 | return deleteRow; 50 | }) 51 | .map((row, idx) => { 52 | const { username, name, id } = followers[idx]; 53 | return { 54 | ...row, 55 | username, 56 | name, 57 | id, 58 | ...followers[idx].public_metrics, 59 | }; 60 | }); 61 | const newFile = xlsx.utils.json_to_sheet(jsonData); 62 | const newWb = xlsx.utils.book_new(); 63 | xlsx.utils.book_append_sheet(newWb, newFile); 64 | const processedFile = xlsx.write(newWb, { 65 | type: "buffer", 66 | bookType: "xlsx", 67 | }); 68 | return res.status(200).send(processedFile); 69 | }); 70 | }); 71 | 72 | app.get("/:name", (req, res) => { 73 | return res.send(`this is test path ${req.params.name}`); 74 | }); 75 | 76 | app.listen(PORT, () => { 77 | console.log(`server listening at http://localhost:${PORT}`); 78 | }); 79 | --------------------------------------------------------------------------------