Skip to content

Commit

Permalink
npm package publish, environment library removal
Browse files Browse the repository at this point in the history
  • Loading branch information
Ranork committed May 30, 2024
1 parent b8119ec commit 5f341a6
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 154 deletions.
12 changes: 0 additions & 12 deletions .env.example

This file was deleted.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
cache
cache
.vscode
17 changes: 0 additions & 17 deletions .vscode/launch.json

This file was deleted.

32 changes: 12 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,44 +19,36 @@ 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
// [TASK] Login
// 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
Expand All @@ -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"]})
Expand Down Expand Up @@ -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 [email protected]


## Contributions
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]> (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"
Expand Down
69 changes: 47 additions & 22 deletions src/controllers/linkedin.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)));

Expand All @@ -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)
Expand All @@ -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());

Expand Down Expand Up @@ -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.');
}
Expand All @@ -117,8 +142,8 @@ class LinkedIn {
* @param {string} parameters.keywords - The keywords to search for
* @param {Array<string>} parameters.network - The network distance (F for 1, S for 2, B for 3+)
* @param {Array<string>} parameters.geoUrn - Locations
* @param {number} limit - Profile object limit (default 100)
* @returns {Promise<Array<Profile>>} Array of profile objects
* @param {number} limit - LinkedinProfile object limit (default 100)
* @returns {Promise<Array<LinkedinProfile>>} Array of profile objects
*/
async searchPeople(parameters, limit = 100) {
console.log('[TASK] Search People: ' + limit + ' (' + JSON.stringify(parameters) + ')');
Expand All @@ -129,36 +154,36 @@ 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); }

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>} Array of profile objects
*/
async extractProfilesFromSearch(page) {
async extractLinkedinProfilesFromSearch(page) {
await page.waitForSelector('.linked-area')

return await page.evaluate(() => {
Expand Down Expand Up @@ -186,4 +211,4 @@ class LinkedIn {
}


module.exports = { LinkedIn }
module.exports = LinkedIn
26 changes: 13 additions & 13 deletions src/controllers/profile.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
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
}


/** 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) {
Expand Down Expand Up @@ -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')

Expand All @@ -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);
Expand All @@ -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)
Expand Down Expand Up @@ -139,4 +139,4 @@ class Profile {
}
}

module.exports = Profile
module.exports = LinkedinProfile
Loading

0 comments on commit 5f341a6

Please sign in to comment.