From c58dab180792ec8d360a3d9ec9e5b6c2ad32c77c Mon Sep 17 00:00:00 2001 From: ION606 Date: Thu, 9 May 2024 08:10:20 -0700 Subject: [PATCH] added npm files --- .gitignore | 2 + README.md | 38 +++- classes/Company.js | 70 ++++++ classes/Profile.js | 47 ++++ classes/classes.js | 7 + classes/misc.js | 30 +++ index.js | 296 +++++++++++++++++++++++++ package-lock.json | 421 ++++++++++++++++++++++++++++++++++++ package.json | 18 ++ src/types/linkedin-api.d.ts | 1 + tsconfig.json | 8 + 11 files changed, 937 insertions(+), 1 deletion(-) create mode 100644 classes/Company.js create mode 100644 classes/Profile.js create mode 100644 classes/classes.js create mode 100644 classes/misc.js create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/types/linkedin-api.d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index c6bba59..7518461 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +temp.js \ No newline at end of file diff --git a/README.md b/README.md index b0f74d0..d703661 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ # linkedin-api -The NodeJS version of tomquirk's linkedin-api python package +An actually useful LinkedIn Nodejs package. + +## Disclaimers +This package is not in any way affiliated with LinkedIn. In fact, your account may be banned for using this (hasn't happened to me though). + +You can use your LinkedIn credentials in code, making this super simple to use! + +*This project was HEAVILY inspired by Tomquirk's LinkedIn-API PyPy package, which you can find at https://github.com/tomquirk/linkedin-api* + +## Quick-Start +```JS +// log in +const LAPI = new linkedInAPIClass(); +await LAPI.login("pinknodders@pinknodders.lol", "**********"); + +// GET a company +const comp = await LAPI.searchCompanies("Linux"); + +// GET one employee +const emp = await comp[0].getEmployees("Torvalds", 1); +``` + +## Contributions +If you want to contribute, just fork, add your features, then make a PR + +If you do contribute, please follow these guidelines +1. Document your changes in the PR (i.e. make a bulleted list of changes) +2. If you change existing code, you must specify that +3. Please please please don't be me and push credentials + +## Reporting Bugs +Please open an issue using the following guildelines +1. Be clear and concise +2. Provide the code necessary to reproduce the problem + +Thanks for using my project! +ION606 \ No newline at end of file diff --git a/classes/Company.js b/classes/Company.js new file mode 100644 index 0000000..89e9744 --- /dev/null +++ b/classes/Company.js @@ -0,0 +1,70 @@ +import { LinkedInProfile } from "./Profile.js"; + +export class Company { + /** @type {import('../index.js').linkedInAPIClass} */ + #APIRef; + + + /** + * gets the employees of a company + * @param {Number} [limit=Infinity] to make things simpler I used increments of 50, so the limit is essentially ceiled to the nearest multiple of 50 + * @param {Boolean} [raw=false] whether or not you want the raw JSON returned + * @returns {Promise} + * @note This function skips over any employees who's profile is marked as private + */ + async getEmployees(limit = Infinity, raw = false) { + const employeesInit = await this.#APIRef._makeReq(`variables=(start:0,origin:FACETED_SEARCH,query:(flagshipSearchIntent:ORGANIZATIONS_PEOPLE_ALUMNI,queryParameters:List((key:currentCompany,value:List(${this.urn})),(key:resultType,value:List(ORGANIZATION_ALUMNI))),includeFiltersInResponse:true),count:1)`) + + const numEmp = employeesInit.data.data.searchDashClustersByAll.paging.total, + empAll = [] + + + // since the max cap is 50, we need to iterate until it's over 50 + for (let i = 0; i < numEmp; i += 50) { + if (empAll.length >= limit) break; + + const c = (i + 50 >= limit) ? limit - i : 50; + + const employeeRes = await this.#APIRef._makeReq(`variables=(start:${i},origin:FACETED_SEARCH,query:(flagshipSearchIntent:ORGANIZATIONS_PEOPLE_ALUMNI,queryParameters:List((key:currentCompany,value:List(${this.urn})),(key:resultType,value:List(ORGANIZATION_ALUMNI))),includeFiltersInResponse:true),count:${c})`); + const employees = employeeRes.included; + + const empParsed = employees.filter(e => (e.$type === "com.linkedin.voyager.dash.search.EntityResultViewModel")) + .filter(o => o.title.text !== "LinkedIn Member"); + + if (empParsed.length) empAll.push(...empParsed); + await this.#APIRef.evade(); + } + + + return (raw) ? empAll : empAll.map(eRaw => new LinkedInProfile(eRaw, this.#APIRef)); + } + + + /** + * @returns {Promise} + * @param {string} name + * @param {boolean} raw + */ + getEmployees = (name, limit=1000, raw=false) => this.#APIRef.searchEmployees(name, limit, !raw, [this.urn]); + + async getInfo() { + const toAdd = `q=universalName&universalName=${this.urn}`; + return await this.#APIRef._makeReq(toAdd); + } + + checkIfCompleted = () => !!(this.name && this.url && this.urn && this.#APIRef); + + /** + * @param {{title: {text: String}, entityUrn: String, navigationUrl: String}} data + * @param {import('../index.js').linkedInAPIClass} APIRef + */ + constructor(data, APIRef) { + this.#APIRef = APIRef; + // this.entityUrn = entityUrn?.replace("urn:li:fsd_company:", ""); + this.name = data.title.text; + this.urn = data.entityUrn; + this.url = data.navigationUrl; + + if (!this.checkIfCompleted()) throw "NOT ALL NEEDED PARAMS FOUND!"; + } +} \ No newline at end of file diff --git a/classes/Profile.js b/classes/Profile.js new file mode 100644 index 0000000..d6fb4cc --- /dev/null +++ b/classes/Profile.js @@ -0,0 +1,47 @@ +import { linkedInAPIClass } from "../index.js"; + +export class LinkedInProfile { + /** @type {linkedInAPIClass} */ + #APIRef; + + /** + * Attempts to get the contact info for the person + * @returns {Promise<{websites: [{label:string, category:string, url:string}], emailAddress:string, phoneNumbers:[string],weChatContactInfo:any,twitterHandles:[string], instantMessengers:[]}>} + */ + async getContactInfo() { + const r = await this.#APIRef._makeReq(`includeWebMetadata=true&variables=(memberIdentity:${this.entityNameJoined})`, true); + if (r?.data?.errors) return console.error(JSON.stringify(r.data.errors)); + const uObj = r.included.find(o => o.publicIdentifier === this.entityNameJoined); + if (!uObj) return; + let { phoneNumbers, emailAddress, websites, weChatContactInfo, twitterHandles, instantMessengers } = uObj; + + if (websites) websites = websites.map(w => ({ label: w.label, category: w.category, url: w.url })); + return { phoneNumbers, emailAddress, websites, weChatContactInfo, twitterHandles, instantMessengers }; + } + + // Parse specific services offered, contained in the 'simpleInsight' section of 'insightsResolutionResults' + parseServices(insights) { + for (let insight of insights) { + if (insight.simpleInsight && insight.simpleInsight.title && insight.simpleInsight.title.text) { + return insight.simpleInsight.title.text; + } + } + return ''; + } + + constructor(jsonData, apiRef) { + this.#APIRef = apiRef; + this.name = jsonData.title ? jsonData.title.text : ''; + this.entityNameJoined = jsonData.navigationUrl?.split("/in/")[1]?.split("?")[0]; + this.profileUrl = jsonData.navigationUrl || ''; + this.trackingUrn = jsonData.trackingUrn || ''; + + const match = jsonData.entityUrn?.match(/urn:li:fsd_profile:(.*?),/); + this.entityUrn = match ? match[1] : ''; + + this.jobServices = jsonData.insightsResolutionResults ? this.parseServices(jsonData.insightsResolutionResults) : ''; + this.primarySubtitle = jsonData.primarySubtitle ? jsonData.primarySubtitle.text : ''; + this.secondarySubtitle = jsonData.secondarySubtitle ? jsonData.secondarySubtitle.text : ''; + this.bserpEntityNavigationalUrl = jsonData.bserpEntityNavigationalUrl || ''; + } +} diff --git a/classes/classes.js b/classes/classes.js new file mode 100644 index 0000000..0a4e8b8 --- /dev/null +++ b/classes/classes.js @@ -0,0 +1,7 @@ +import linkedInAPIClass from "../index.js"; +import { LinkedInProfile } from "./Profile.js"; +import { Company } from "./Company.js"; +import { GenericEntity, Group, ReactionTypeCount, SocialActivityCounts } from './misc.js' + + +export { linkedInAPIClass, LinkedInProfile, Company, GenericEntity, Group, ReactionTypeCount, SocialActivityCounts }; \ No newline at end of file diff --git a/classes/misc.js b/classes/misc.js new file mode 100644 index 0000000..0197e5d --- /dev/null +++ b/classes/misc.js @@ -0,0 +1,30 @@ +export class ReactionTypeCount { + constructor({ count, reactionType }) { + this.count = count; + this.reactionType = reactionType; + } +} + +export class SocialActivityCounts { + constructor({ entityUrn, numComments, numLikes, reactionTypeCounts, liked, preDashEntityUrn }) { + this.entityUrn = entityUrn; + this.numComments = numComments; + this.numLikes = numLikes; + this.reactionTypeCounts = reactionTypeCounts.map(count => new ReactionTypeCount(count)); + this.liked = liked; + this.preDashEntityUrn = preDashEntityUrn; + } +} + +export class Group { + constructor({ entityUrn }) { + this.entityUrn = entityUrn; + } +} + +// Placeholder class for any other types +export class GenericEntity { + constructor(data) { + Object.assign(this, data); + } +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..d9db9bd --- /dev/null +++ b/index.js @@ -0,0 +1,296 @@ +import fs from 'fs'; +import { wrapper } from 'axios-cookiejar-support'; +import { CookieJar } from 'tough-cookie'; +import axiosModule from 'axios'; +import { LinkedInProfile, Company, GenericEntity, Group, SocialActivityCounts } from './classes/classes.js'; + +const cookieJar = new CookieJar(); +const axios = wrapper(axiosModule.create({ + jar: cookieJar, // attach cookie jar + withCredentials: true +})); + + +export const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +export const randomIntFromInterval = (boundLower, boundUpper) => Math.floor(Math.random() * (boundUpper - boundLower + 1) + boundLower); +export const inRange = (inp, minAmt, maxAmt) => (inp >= minAmt && inp <= maxAmt); + + +/** + * Function to dynamically create class instances based on type + * @param {Object[]} included + * @param {linkedInAPIClass} APIRef + * @param {boolean} [excludeGeneric=false] whether or not to exclude any object that does not exactly fit the Company type + * @returns {[SocialActivityCounts | Group | Company | GenericEntity]} + */ +function parseIncludedData(included, APIRef, excludeGeneric = false) { + const classMap = { + 'com.linkedin.voyager.dash.feed.SocialActivityCounts': SocialActivityCounts, + 'com.linkedin.voyager.dash.groups.Group': Group, + "com.linkedin.voyager.dash.search.EntityResultViewModel": Company, + // 'com.linkedin.voyager.dash.organization.Company': Company, + 'default': GenericEntity // Default class for any other type + }, + toIgnore = { + "com.linkedin.voyager.dash.search.FeedbackCard": -1, + "com.linkedin.voyager.dash.search.LazyLoadedActions": -1, // companies again + 'com.linkedin.voyager.dash.organization.Company': -1 + } + + return included.map(item => { + const EntityClass = classMap[item.$type] || toIgnore[item.$type] || classMap['default']; + if (EntityClass === -1) return; + + // ignore the strange companies like "linkedIn pages" + // if (!item.name) return null; + + // APIRef may be ignored here + return new EntityClass(item, APIRef); + }).filter(o => (excludeGeneric && o instanceof Company) || (!excludeGeneric && o)); +} + + +export async function parseResponse(data, APIRef, excludeGeneric) { + const jsonData = (typeof data === 'string') ? JSON.parse(data) : data; + const includedData = jsonData.included || []; + return parseIncludedData(includedData, APIRef, excludeGeneric); +} + + +function findRangeIndex(number) { + const rangeMap = ["B", "C", "D", "E", "F", "G", "H", "I"] + const companySizeRanges = [ + [1, 10], + [11, 50], + [51, 200], + [201, 500], + [501, 1000], + [1001, 5000], + [5001, 10000], + [10001, Infinity], + ]; + for (let i = 0; i < companySizeRanges.length; i++) { + const [lowerBound, upperBound] = companySizeRanges[i]; + if (number >= lowerBound && number <= upperBound) { + return `"${rangeMap[i]}"`; + } + } + + return -1; +} + + +/** + * @returns {String} + * @param {Array} nums + */ +export function numsToSizes(...nums) { + if (nums.length > 8) throw "MUST PROVIDE A 8 OR LESS RANGES"; + + const ranges = nums.map(findRangeIndex); + if (ranges.includes(-1)) throw `${nums} CONTAINS AN INVALID RANGE!`; + return `[${ranges.join("%2C")}]`; +} + + +/** + * @returns {String} + * @param {Array<1 | 2 | 3>} nums + */ +export const numToConDegs = (nums) => nums.map((n) => { + if (n === 1) return "F"; + else if (n === 2) return "S"; + else if (n === 3) return "O"; + else return null; +}).filter(o => o); + + +// a "collection" of helper functions +class APIHelper { + parseCookiesToMap(cookies) { + const cookieMap = new Map(); + cookies.forEach(cookie => { + const parts = cookie.split(';'); // Split cookie string into parts by ';' + const firstPart = parts[0]; // The first part contains the name and value + const [name, value] = firstPart.split('='); // Split the first part to get name and value + cookieMap.set(name.trim(), value.trim().replace(/"/g, '')); // Trim and remove quotes, then add to map + }); + return cookieMap; + } + + + getCookies(username, password) { + return new Promise(async (resolve, reject) => { + const spawn = (await import("child_process")).spawn; + const pythonProcess = spawn('python', ["auth.py", username, password]); + pythonProcess.stdout.on('data', (data) => resolve(data.toString())); + }); + } + + /** + * @deprecated + */ + async getCookiesOld(username, password) { + const authurl = 'https://www.linkedin.com/uas/authenticate' + const res = await axios.get(authurl, { + headers: this.authheaders + }); + + const scookie = this.parseCookiesToMap(res.headers['set-cookie']); + + const payload = { + session_key: username, + session_password: password, + JSESSIONID: scookie.get('JSESSIONID') + }; + + console.log(payload); + + // this.authheaders['cookies'] = res.headers['set-cookie']; + + try { + const response = await axios.post( + authurl, + payload, + { + withCredentials: true, + headers: this.authheaders, + cookies: cookieJar + // Commenting out the proxy setting as Axios requires special handling for proxies + // proxy: proxies + } + ); + + console.log(response.headers); + return response.data; + } catch (error) { + console.log(error); + console.error('Error during authentication:', error.response ? error.response.data : error.message); + } + } + + constructor() { + this.authheaders = { + "X-Li-User-Agent": "LIAuthLibrary:0.0.3 com.linkedin.android:4.1.881 Asus_ASUS_Z01QD:android_9", + "User-Agent": "ANDROID OS", + "X-User-Language": "en", + "X-User-Locale": "en_us", + "Accept-Language": "en-us" + } + } +} + + +export default class linkedInAPIClass { + /** + * A function to try and trick the LinkedIn API rate limit/bot detector + */ + evade = () => wait(randomIntFromInterval(2, 5) * 1000); + + /** + * @param {String} keyword i.e. "biotechnology" + * @param {Array?} numEmp + * @param {Number?} [start=0] + * @param {boolean?} [castToClass=true] whether the function should return a list of Company classes or just raw JSON + */ + async searchCompanies(keyword, numEmp, start = 0, castToClass = true, excludeGeneric = false) { + let urlExt = `variables=(start:${start},origin:GLOBAL_SEARCH_HEADER,query:(keywords:${keyword},flagshipSearchIntent:SEARCH_SRP,queryParameters:List((key:resultType,value:List(COMPANIES))),includeFiltersInResponse:false))`; + if (numEmp) urlExt += `&companySize=${numsToSizes(...numEmp)}`; + const r = await this._makeReq(urlExt); + if (!castToClass) return r; + else return parseResponse(r, this, excludeGeneric); + } + + + /** + * @returns {Promise} + * @param {String} keyword the user to search for + * @param {Number?} [limit=1000] + * @param {boolean?} [castToClass=true] whether the function should return a list of Company classes or just raw JSON + * @param {Array?} currentCompanies + * @param {Array<1 | 2 | 3>?} conDeg the level(s) of connection to include (defults to all) + * @example + * // search for "John Appleseed" (who works at github), but only the first 50 results who are also first connections + * LAPI.searchEmployees("John Appleseed", 50, true, ["1418841"], [1]) + */ + async searchEmployees(keyword, limit = 1000, castToClass = true, currentCompanies = [], conDeg = []) { + const empAll = []; + for (let i = 0; i < limit; i += 50) { + if (empAll.length >= limit) break; + let urlExt = `includeWebMetadata=true&variables=(start:${i},query:(keywords:${keyword},flagshipSearchIntent:SEARCH_SRP,queryParameters:List((key:resultType,value:List(PEOPLE)))))`; + + if (currentCompanies.length) urlExt += `¤tCompany=["${currentCompanies.join('"%2C"')}"]`; + if (conDeg.length) { + const conFiltered = numToConDegs(conDeg); + if (conFiltered.length) urlExt += `network=["${currentCompanies.join('"%2C"')}"]}` + } + + const r = await this._makeReq(urlExt); + if (!r) return []; + + const filtered = r.included.filter(e => e.template === 'UNIVERSAL'); + + if (filtered.length) empAll.push(...filtered); + await this.evade(); + } + + if (!castToClass) return empAll; + else return empAll.map(o => new LinkedInProfile(o, this)); + } + + + /** + * @param {String} reqPath + * @returns {Promise} + * @throws The promise will reject on error + */ + async _makeReq(reqPath, isProfile = false) { + await this.evade(); + const res = await axios.get(`https://www.linkedin.com/voyager/api/graphql?${reqPath}&queryId=${(!isProfile) ? "voyagerSearchDashClusters.f0c4f21d8a526c4a5dd0ae253c9b6e02" : "voyagerIdentityDashProfiles.5a6722404e6afd08958f5105e51cad51"}`, { headers: this.headers }).catch((err) => { + console.error(err); + return null; + }); + + return res?.data; + } + + // attempts to use cookies and otherwise uses auth + async login(username, password) { + let cookie; + if (this.resetCookies || !fs.existsSync("cookie.txt")) { + cookie = await this.helper.getCookies(username, password); + console.log(cookie); + + if (cookie['login_result'] != "CHALLENGE") throw `\n\nIP IS NOT AUTHORIZED, PLEASE LOG IN THROUGH YOUR BROWSER USING:\nusername: ${username}\npassword: ${password}\n\n`; + if (cookie['login_result'] != "PASS") throw cookie; + + // const sessioncookie = await this.#getCookies(); + // throw "NO COOKIE FOUND!"; + } else cookie = fs.readFileSync('cookie.txt'); + + this.headers = { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0', + 'Accept': 'application/vnd.linkedin.normalized+json+2.1', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br', + 'x-li-lang': 'en_US', + 'x-li-track': '{"clientVersion":"1.13.14725","mpVersion":"1.13.14725","osName":"web","timezoneOffset":-7,"timezone":"America/Los_Angeles","deviceFormFactor":"DESKTOP","mpName":"voyager-web","displayDensity":1,"displayWidth":1920,"displayHeight":1080}', + 'x-li-page-instance': 'urn:li:page:d_flagship3_search_srp_jobs;TvndZ7TATTy2i/ZUyyD3Zg==', + 'csrf-token': 'ajax:2538600735149500238', + 'x-restli-protocol-version': '2.0.0', + 'x-li-pem-metadata': 'Voyager - Organization - LCP_Member=interest-pipeline', + 'Connection': 'keep-alive', + 'Referer': 'https://www.linkedin.com/jobs/search/?currentJobId=3840672849&distance=25.0&geoId=103644278&keywords=temp&origin=HISTORY', + 'Cookie': cookie, + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + 'TE': 'trailers' + }; + } + + constructor(resetCookies = false) { + this.resetCookies = resetCookies; + this.helper = new APIHelper(); + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e18ffb2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,421 @@ +{ + "name": "linkedin-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "linkedin-api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.6.8", + "axios-cookiejar-support": "^5.0.1", + "cheerio": "^1.0.0-rc.12", + "tough-cookie": "^4.1.3" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-cookiejar-support": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/axios-cookiejar-support/-/axios-cookiejar-support-5.0.1.tgz", + "integrity": "sha512-lwjyusKDJ1dLMRPnARWJCdcRXqmYvNITHnCQHRJGtIIL2QXj/lYPjMg04XZDp8QItzluu0KJqbjYlvI0+5X5Xw==", + "dependencies": { + "http-cookie-agent": "^6.0.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "axios": ">=0.20.0", + "tough-cookie": ">=4.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cookie-agent": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-6.0.3.tgz", + "integrity": "sha512-6JdymEgWgsg9VQ5VN9FGpRRcivyu4WdM0Ud3kW+Q0PB7knt0EFtlhNPU8wCuscXLfIEI5y6jEMdFTBODNsJR6g==", + "dependencies": { + "agent-base": "^7.1.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/3846masa" + }, + "peerDependencies": { + "deasync": "^0.1.26", + "tough-cookie": "^4.0.0", + "undici": "^5.11.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "deasync": { + "optional": true + }, + "undici": { + "optional": true + } + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ef1561 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "dependencies": { + "axios": "^1.6.8", + "axios-cookiejar-support": "^5.0.1", + "cheerio": "^1.0.0-rc.12", + "tough-cookie": "^4.1.3" + }, + "type": "module", + "name": "linkedin-api-js", + "version": "1.0.0-0", + "main": "APIMain.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "ION606", + "license": "ISC", + "description": "" +} diff --git a/src/types/linkedin-api.d.ts b/src/types/linkedin-api.d.ts new file mode 100644 index 0000000..f2ce307 --- /dev/null +++ b/src/types/linkedin-api.d.ts @@ -0,0 +1 @@ +declare module 'linkedin-api'; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4184695 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ] + } +} \ No newline at end of file