From 5f341a6282139fdbdaa32190d859469d6590119e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20O=C4=9EUZ?= Date: Thu, 30 May 2024 21:56:19 +0300 Subject: [PATCH] npm package publish, environment library removal --- .env.example | 12 ------- .gitignore | 3 +- .vscode/launch.json | 17 --------- README.md | 32 +++++++---------- package.json | 14 +++++--- src/controllers/linkedin.js | 69 ++++++++++++++++++++++++------------ src/controllers/profile.js | 26 +++++++------- src/libraries/environment.js | 20 ----------- src/libraries/misc.js | 7 ++-- src/main.js | 45 ++++------------------- 10 files changed, 91 insertions(+), 154 deletions(-) delete mode 100644 .env.example delete mode 100644 .vscode/launch.json delete mode 100644 src/libraries/environment.js diff --git a/.env.example b/.env.example deleted file mode 100644 index 95547a5..0000000 --- a/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -MAIN_ADDRESS="https://www.linkedin.com/" - -TIMEOUT=60 //* Seconds - - -ACCOUNT_USERNAME="" -ACCOUNT_PASSWORD="" - - -PROFILEBUTTON_MESSAGE="Mesaj" -PROFILEBUTTON_CONNECT="Bağlantı kur" -PROFILEBUTTON_FOLLOW="Takip Et" \ No newline at end of file diff --git a/.gitignore b/.gitignore index be67582..2053661 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* -cache \ No newline at end of file +cache +.vscode \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 550cead..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "skipFiles": [ - "/**" - ], - "program": "${workspaceFolder}\\src\\main.js" - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index e75b7d4..153ff39 100644 --- a/README.md +++ b/README.md @@ -19,37 +19,28 @@ Auto LinkedIn is a project that provides automation for LinkedIn using Node.js a - Puppeteer library is used, which requires Chrome browser for automation. ### Installation Steps -1. Clone the project: +1. Create a new directory ```bash -git clone https://github.com/Ranork/Auto-Linkedin.git +mkdir linkedinAutomationProject +cd linkedinAutomationProject ``` -2. Navigate to the project directory: +2. Install NPM ```bash -cd Auto-Linkedin -mkdir cache +npm init -y ``` -3. Install the dependencies: +3. Install package: ```bash -npm install +npm install auto-linkedin ``` -## Usage -1. Open the `.env` file and configure your LinkedIn account credentials (username & password) and other necessary settings for automation tasks. - -2. Run the application: -```bash -npm run start -``` - - -### Example Usage +### Usage 1. Create a linkedin client and login: ```js -const client = new LinkedIn({ headless: true }) +const client = new LinkedIn() await client.login(process.env.USERNAME, process.env.PASSWORD) //-- Console @@ -57,6 +48,7 @@ await client.login(process.env.USERNAME, process.env.PASSWORD) // New Browser created. // Login completed. ``` +Follow the console even though there is an extra instruction. 2. Search for users with keyword and 2. network distance (200 limit): ```js @@ -65,7 +57,7 @@ const profiles = await client.searchPeople({ network: ['S'] }, 200) -// profiles = [Profile, Profile, ...] +// profiles = [LinkedinProfile, LinkedinProfile, ...] //-- Console // [TASK] Search People: 200 ({"keywords":"venture capital","network":["S"]}) @@ -93,7 +85,7 @@ for (let p of profiles) { ## Contact -For any questions or feedback about the project, please contact us through GitHub. +For any questions or feedback about the project, please contact us through GitHub or emir@akatron.net ## Contributions diff --git a/package.json b/package.json index d669c62..fd34e1a 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,19 @@ { "name": "auto-linkedin", - "version": "1.0.0", - "description": "", + "version": "1.1.0", + "description": "Auto LinkedIn is a project that provides automation for LinkedIn using Node.js and Puppeteer. This project helps you save time by automating various tasks on LinkedIn.", "main": "src/main.js", "scripts": { "start": "node src/main.js", "test": "echo \"Error: no test specified\" && exit 1" }, - "keywords": [], - "author": "", - "license": "ISC", + "keywords": ["linkedin", "social media", "automation", "puppeteer"], + "author": "Ranork (https://github.com/Ranork)", + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/Ranork/Auto-Linkedin" + }, "dependencies": { "dotenv": "^16.4.5", "puppeteer": "^22.8.1" diff --git a/src/controllers/linkedin.js b/src/controllers/linkedin.js index 925806e..d512440 100644 --- a/src/controllers/linkedin.js +++ b/src/controllers/linkedin.js @@ -1,17 +1,44 @@ const { default: puppeteer } = require("puppeteer"); -const { Environment } = require("../libraries/environment") const querystring = require('querystring'); const fs = require('fs'); -const Profile = require("./profile"); +const LinkedinProfile = require("./profile"); const { randomNumber } = require("../libraries/misc"); class LinkedIn { /** Linkedin Client * @param {PuppeteerLaunchOptions} browserSettings - Puppeteer browser settings + * + * @param {Object} linkedinSettings - Settings for linkedin static variables + * + * @param {string} linkedinSettings.MAIN_ADDRESS - Main linkedin address (default: "https://www.linkedin.com/") + * @param {string} linkedinSettings.CACHE_DIR - cache files dir (default: "./cache/") + * + * @param {string} linkedinSettings.PROFILEBUTTON_MESSAGE - In profile message button's text (default: "Message") + * @param {string} linkedinSettings.PROFILEBUTTON_CONNECT - In profile connect button's text (default: "Connect") + * @param {string} linkedinSettings.PROFILEBUTTON_FOLLOW - In profile follow button's text (default: "Follow") + * + * @param {number} linkedinSettings.COOLDOWN_MIN - Minimum cooldown treshold (default: 5) + * @param {number} linkedinSettings.COOLDOWN_MAX - Maximum cooldown treshold (default: 20) + * @param {number} linkedinSettings.TIMEOUT - Timeout in seconds (default: 60) */ - constructor(browserSettings) { + constructor(browserSettings, linkedinSettings) { this.browserSettings = browserSettings + this.linkedinSettings = linkedinSettings || {} + + if (!this.linkedinSettings.MAIN_ADDRESS) this.linkedinSettings.MAIN_ADDRESS = process.env.MAIN_ADDRESS || 'https://www.linkedin.com/' + if (!this.linkedinSettings.CACHE_DIR) this.linkedinSettings.CACHE_DIR = process.env.CACHE_DIR || './cache/' + + if (!this.linkedinSettings.PROFILEBUTTON_MESSAGE) this.linkedinSettings.PROFILEBUTTON_MESSAGE = process.env.PROFILEBUTTON_MESSAGE || 'Message' + if (!this.linkedinSettings.PROFILEBUTTON_CONNECT) this.linkedinSettings.PROFILEBUTTON_CONNECT = process.env.PROFILEBUTTON_CONNECT || 'Connect' + if (!this.linkedinSettings.PROFILEBUTTON_FOLLOW) this.linkedinSettings.PROFILEBUTTON_FOLLOW = process.env.PROFILEBUTTON_FOLLOW || 'Follow' + + if (!this.linkedinSettings.COOLDOWN_MIN) this.linkedinSettings.COOLDOWN_MIN = parseInt(process.env.COOLDOWN_MIN) || 5 + if (!this.linkedinSettings.COOLDOWN_MAX) this.linkedinSettings.COOLDOWN_MAX = parseInt(process.env.COOLDOWN_MAX) || 20 + if (!this.linkedinSettings.TIMEOUT) this.linkedinSettings.TIMEOUT = parseInt(process.env.TIMEOUT) || 60 + + // make dir if not exists + if (!fs.existsSync(this.linkedinSettings.CACHE_DIR)) fs.mkdirSync(this.linkedinSettings.CACHE_DIR, { recursive: true }); } /** Get client's browser @@ -33,16 +60,14 @@ class LinkedIn { */ async login(username, password) { console.log('[TASK] Login'); - - await Environment.declare_settings() const browser = await this.getBrowser() const page = await browser.newPage() - if (fs.existsSync('./cache/cookies.json')) { - let cookies = JSON.parse(fs.readFileSync('./cache/cookies.json')) + if (fs.existsSync(this.linkedinSettings.CACHE_DIR + 'cookies.json')) { + let cookies = JSON.parse(fs.readFileSync(this.linkedinSettings.CACHE_DIR + 'cookies.json')) await page.setCookie(...cookies) - await page.goto(Environment.settings.MAIN_ADDRESS + 'feed') + await page.goto(this.linkedinSettings.MAIN_ADDRESS + 'feed') await new Promise(r => setTimeout(r, randomNumber(1,3))); @@ -52,7 +77,7 @@ class LinkedIn { } } - await page.goto(Environment.settings.MAIN_ADDRESS + 'login') + await page.goto(this.linkedinSettings.MAIN_ADDRESS + 'login') const usernameInput = await page.$('#username') await usernameInput.type(username) @@ -70,7 +95,7 @@ class LinkedIn { //* Checkpoint for login if (afterLoginUrl.includes('checkpoint/challenge')) { - for (let i = 0; i < Environment.settings.TIMEOUT; i++) { + for (let i = 0; i < this.linkedinSettings.TIMEOUT; i++) { if (page.url() !== afterLoginUrl) { console.log(' New URL: ' + page.url()); @@ -101,7 +126,7 @@ class LinkedIn { if (page.url().includes('feed')) { const cookies = await page.cookies() - fs.writeFileSync('./cache/cookies.json', JSON.stringify(cookies)) + fs.writeFileSync(this.linkedinSettings.CACHE_DIR + 'cookies.json', JSON.stringify(cookies)) await page.close() return console.log(' Login complated.'); } @@ -117,8 +142,8 @@ class LinkedIn { * @param {string} parameters.keywords - The keywords to search for * @param {Array} parameters.network - The network distance (F for 1, S for 2, B for 3+) * @param {Array} parameters.geoUrn - Locations - * @param {number} limit - Profile object limit (default 100) - * @returns {Promise>} Array of profile objects + * @param {number} limit - LinkedinProfile object limit (default 100) + * @returns {Promise>} Array of profile objects */ async searchPeople(parameters, limit = 100) { console.log('[TASK] Search People: ' + limit + ' (' + JSON.stringify(parameters) + ')'); @@ -129,17 +154,17 @@ class LinkedIn { if (parameters.geoUrn) { parameters.geoUrn = JSON.stringify(parameters.geoUrn)} let i = 1 - let findedProfiles = [] + let findedLinkedinProfiles = [] for (let p = 1; p <= limit / 10; p++) { parameters.page = p const qString = querystring.stringify(parameters) - await page.goto(Environment.settings.MAIN_ADDRESS + 'search/results/people/?' + qString) + await page.goto(this.linkedinSettings.MAIN_ADDRESS + 'search/results/people/?' + qString) try { - let profiles = await this.extractProfilesFromSearch(page) - findedProfiles.push(...profiles) + let profiles = await this.extractLinkedinProfilesFromSearch(page) + findedLinkedinProfiles.push(...profiles) console.log(' Page: ' + i + '/' + (limit / 10) + ' -> ' + profiles.length); } catch (e) { console.log(e); } @@ -147,18 +172,18 @@ class LinkedIn { i++ } - console.log(' Search complete: ' + findedProfiles.length); + console.log(' Search complete: ' + findedLinkedinProfiles.length); await page.close() - return findedProfiles.map(p => (new Profile(p))) + return findedLinkedinProfiles.map(p => (new LinkedinProfile(p))) } - /** Extract Profiles from Search People Page + /** Extract LinkedinProfiles from Search People Page * @param {Object} page - the puppeteer page that opened in search/results/people url * @returns {Promise} Array of profile objects */ - async extractProfilesFromSearch(page) { + async extractLinkedinProfilesFromSearch(page) { await page.waitForSelector('.linked-area') return await page.evaluate(() => { @@ -186,4 +211,4 @@ class LinkedIn { } -module.exports = { LinkedIn } \ No newline at end of file +module.exports = LinkedIn \ No newline at end of file diff --git a/src/controllers/profile.js b/src/controllers/profile.js index 5166d91..d224559 100644 --- a/src/controllers/profile.js +++ b/src/controllers/profile.js @@ -1,9 +1,8 @@ -const { Environment } = require("../libraries/environment"); const { randomNumber } = require("../libraries/misc"); -const { LinkedIn } = require("./linkedin"); +const LinkedIn = require("./linkedin"); -class Profile { +class LinkedinProfile { constructor(details) { this.details = details } @@ -11,15 +10,16 @@ class Profile { /** Visit the user's rofile page * @param {LinkedIn} linkedinClient - Client that will used in visit - * @param {number} waitMs - Wait milliseconds after opening profile (default is 500ms) + * @param {number} waitMs - Wait milliseconds after opening profile (default is coming from linkedin client) + * @param {boolean} scrollPage - Scroll page to bottom to be sure (default: true) */ async visitProfile(linkedinClient, waitMs, scrollPage = true) { - if (!waitMs) waitMs = randomNumber() + if (!waitMs) waitMs = randomNumber(linkedinClient.linkedinSettings.COOLDOWN_MIN, linkedinClient.linkedinSettings.COOLDOWN_MAX) - console.log('[TASK] Profile Visit: ' + this.details.name + ' (waitMs: ' + waitMs.toFixed(2) + ', scrollPage: ' + scrollPage + ')'); + console.log('[TASK] LinkedinProfile Visit: ' + this.details.name + ' (waitMs: ' + waitMs.toFixed(2) + ', scrollPage: ' + scrollPage + ')'); const browser = await linkedinClient.getBrowser() const page = await browser.newPage() - await page.goto(Environment.settings.MAIN_ADDRESS + 'in/' + this.details.id) + await page.goto(linkedinClient.linkedinSettings.MAIN_ADDRESS + 'in/' + this.details.id) await page.waitForSelector('.scaffold-layout__main') if (scrollPage) { @@ -52,12 +52,12 @@ class Profile { * @param {string} connectionMessage - Message that will send with connection request */ async connectionRequest(linkedinClient, connectionMessage, waitMs) { - if (!waitMs) waitMs = randomNumber() + if (!waitMs) waitMs = randomNumber(linkedinClient.linkedinSettings.COOLDOWN_MIN, linkedinClient.linkedinSettings.COOLDOWN_MAX) console.log('[TASK] Conection request: ' + this.details.name + ' (waitMs: ' + waitMs.toFixed(2) + ')'); const browser = await linkedinClient.getBrowser() const page = await browser.newPage() - await page.goto(Environment.settings.MAIN_ADDRESS + 'in/' + this.details.id) + await page.goto(linkedinClient.linkedinSettings.MAIN_ADDRESS + 'in/' + this.details.id) await page.waitForSelector('.scaffold-layout__main > section > div:nth-child(2) > div:last-child > div > button') @@ -71,13 +71,13 @@ class Profile { return parentDiv.querySelector('button').textContent.trim() }) - const firstButtonisMessage = (buttonText === Environment.settings.PROFILEBUTTON_MESSAGE) + const firstButtonisMessage = (buttonText === linkedinClient.linkedinSettings.PROFILEBUTTON_MESSAGE) if (firstButtonisMessage) { await page.close() return console.log(' Already connected to ' + this.details.name + ' (' + this.details.id + ')') } - const firstButtonisConnect = (buttonText === Environment.settings.PROFILEBUTTON_CONNECT) + const firstButtonisConnect = (buttonText === linkedinClient.linkedinSettings.PROFILEBUTTON_CONNECT) if (firstButtonisConnect) { let connectButtonQuery = '.scaffold-layout__main > section > div:nth-child(2) > div:last-child > div > button' await page.waitForSelector(connectButtonQuery); @@ -104,7 +104,7 @@ class Profile { return console.log(' Connection request send to ' + this.details.name + ' (' + this.details.id + ')') } - const firstButtonisFollow = (buttonText === Environment.settings.PROFILEBUTTON_FOLLOW) + const firstButtonisFollow = (buttonText === linkedinClient.linkedinSettings.PROFILEBUTTON_FOLLOW) if (firstButtonisFollow) { let moreButtonQuery = '.scaffold-layout__main > section > div:nth-child(2) > div:last-child > div > div:last-child > button' await page.waitForSelector(moreButtonQuery) @@ -139,4 +139,4 @@ class Profile { } } -module.exports = Profile \ No newline at end of file +module.exports = LinkedinProfile \ No newline at end of file diff --git a/src/libraries/environment.js b/src/libraries/environment.js deleted file mode 100644 index 2bfd49b..0000000 --- a/src/libraries/environment.js +++ /dev/null @@ -1,20 +0,0 @@ -require('dotenv').config({overwrite: true}); - - -class Environment { - static settings = {} - - static async declare_settings() { - this.settings = {...process.env} - - this.settings.TIMEOUT = parseInt(process.env.TIMEOUT) - this.settings.COOLDOWN_MIN = parseInt(process.env.COOLDOWN_MIN) - this.settings.COOLDOWN_MAX = parseInt(process.env.COOLDOWN_MAX) - - console.log('[S] Environment Declared.'); - } -} - - - -module.exports = { Environment } \ No newline at end of file diff --git a/src/libraries/misc.js b/src/libraries/misc.js index c0ac709..aa1e40d 100644 --- a/src/libraries/misc.js +++ b/src/libraries/misc.js @@ -1,9 +1,6 @@ -const { Environment } = require("./environment") - - function randomNumber(start, end) { - if (!start) start = Environment.settings.COOLDOWN_MIN - if (!end) end = Environment.settings.COOLDOWN_MAX + if (!start) start = 5 + if (!end) end = 20 return (Math.random() * Math.abs(end - start)) + start } diff --git a/src/main.js b/src/main.js index 89887d1..875bb76 100644 --- a/src/main.js +++ b/src/main.js @@ -1,41 +1,8 @@ -const { Environment } = require("./libraries/environment"); -const { LinkedIn } = require("./controllers/linkedin"); +const LinkedIn = require("./controllers/linkedin"); +const LinkedinProfile = require("./controllers/profile"); -async function main() { - await Environment.declare_settings() - //! Headless unabled for debug - const client = new LinkedIn({ headless: true }) - await client.login(Environment.settings.ACCOUNT_USERNAME, Environment.settings.ACCOUNT_PASSWORD) - - let profiles = await client.searchPeople({ - keywords: 'Data Analyst', - network: ['S'], - // geoUrn: ['90010435'], - }, 50) - - let i = 1 - for (let p of profiles) { - try { - await p.connectionRequest(client, `Merhaba, ben DEBI'den Emir. Veri analizini dinamik hale getirerek teknik bilgi olmadan rapor hazırlamayı mümkün kılıyoruz! -Sizin fikirleriniz bizim için değerli. -DEBI'yi denemek için: https://debi.akatron.net/demo/ -Bana ulaşmak için: emir@akatron.net`) - } - catch (e) { - console.log(e); - } - console.log(' ' + i + '/' + profiles.length); - i++ - } - - - - -} - - - - - -main() \ No newline at end of file +module.exports = { + LinkedIn, + LinkedinProfile, +} \ No newline at end of file