linkedin-api-voyager 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/utils.js ADDED
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.filterKeys = filterKeys;
15
+ exports.filterOutKeys = filterOutKeys;
16
+ exports.getNestedValue = getNestedValue;
17
+ exports.extractFields = extractFields;
18
+ exports.debugObjectStructure = debugObjectStructure;
19
+ exports.resolveReferences = resolveReferences;
20
+ exports.extractDataWithReferences = extractDataWithReferences;
21
+ exports.debugResolvedStructure = debugResolvedStructure;
22
+ exports.extractFieldsFromIncluded = extractFieldsFromIncluded;
23
+ exports.mergeExtraFields = mergeExtraFields;
24
+ function filterKeys(obj, keysToKeep) {
25
+ const filteredObject = {};
26
+ keysToKeep.forEach((key) => {
27
+ if (obj.hasOwnProperty(key)) {
28
+ filteredObject[key] = obj[key];
29
+ }
30
+ });
31
+ return filteredObject;
32
+ }
33
+ function filterOutKeys(obj, keysToIgnore) {
34
+ const filteredObject = {};
35
+ Object.keys(obj).forEach((key) => {
36
+ if (!keysToIgnore.includes(key)) {
37
+ filteredObject[key] = obj[key];
38
+ }
39
+ });
40
+ return filteredObject;
41
+ }
42
+ // Nova função para extrair valores de caminhos aninhados
43
+ function getNestedValue(obj, path) {
44
+ return path.split('.').reduce((current, key) => {
45
+ var _a;
46
+ // Lidar com arrays como attributes[0]
47
+ if (key.includes('[') && key.includes(']')) {
48
+ const [arrayKey, indexStr] = key.split('[');
49
+ const index = parseInt(indexStr.replace(']', ''));
50
+ return (_a = current === null || current === void 0 ? void 0 : current[arrayKey]) === null || _a === void 0 ? void 0 : _a[index];
51
+ }
52
+ return current === null || current === void 0 ? void 0 : current[key];
53
+ }, obj);
54
+ }
55
+ // Nova função melhorada para filtrar com caminhos aninhados
56
+ function extractFields(data, fieldsMap) {
57
+ return data.map(item => {
58
+ const extracted = {};
59
+ Object.entries(fieldsMap).forEach(([newKey, path]) => {
60
+ const value = getNestedValue(item, path);
61
+ if (value !== undefined) {
62
+ extracted[newKey] = value;
63
+ }
64
+ });
65
+ return extracted;
66
+ });
67
+ }
68
+ // Função para debug - mostra a estrutura do objeto
69
+ function debugObjectStructure(obj, maxDepth = 3, currentDepth = 0) {
70
+ if (currentDepth >= maxDepth)
71
+ return;
72
+ const indent = ' '.repeat(currentDepth);
73
+ if (Array.isArray(obj)) {
74
+ console.log(`${indent}Array[${obj.length}]:`);
75
+ if (obj.length > 0) {
76
+ console.log(`${indent} [0]:`);
77
+ debugObjectStructure(obj[0], maxDepth, currentDepth + 2);
78
+ }
79
+ }
80
+ else if (obj && typeof obj === 'object') {
81
+ Object.keys(obj).slice(0, 10).forEach(key => {
82
+ const value = obj[key];
83
+ if (typeof value === 'object' && value !== null) {
84
+ console.log(`${indent}${key}:`);
85
+ debugObjectStructure(value, maxDepth, currentDepth + 1);
86
+ }
87
+ else {
88
+ console.log(`${indent}${key}: ${typeof value} = ${String(value).slice(0, 50)}...`);
89
+ }
90
+ });
91
+ }
92
+ }
93
+ // Função para resolver referências URN dinamicamente
94
+ function resolveReferences(data, included) {
95
+ if (!data || !included)
96
+ return data;
97
+ // Criar um mapa de URN para acesso rápido
98
+ const urnMap = new Map();
99
+ included.forEach(item => {
100
+ if (item.entityUrn) {
101
+ urnMap.set(item.entityUrn, item);
102
+ }
103
+ });
104
+ // Função recursiva para resolver referências
105
+ function resolveObject(obj) {
106
+ if (Array.isArray(obj)) {
107
+ return obj.map(item => resolveObject(item));
108
+ }
109
+ if (obj && typeof obj === 'object') {
110
+ const resolved = {};
111
+ Object.entries(obj).forEach(([key, value]) => {
112
+ // Detectar chaves que começam com * (referências URN)
113
+ if (key.startsWith('*') && typeof value === 'string') {
114
+ const referencedData = urnMap.get(value);
115
+ if (referencedData) {
116
+ // Remover o * e usar como chave
117
+ const cleanKey = key.substring(1);
118
+ resolved[cleanKey] = resolveObject(referencedData);
119
+ }
120
+ else {
121
+ resolved[key] = value; // Manter original se não encontrar
122
+ }
123
+ }
124
+ // Detectar arrays de URNs
125
+ else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string' && value[0].startsWith('urn:li:')) {
126
+ const resolvedArray = value.map(urn => {
127
+ const referencedData = urnMap.get(urn);
128
+ return referencedData ? resolveObject(referencedData) : urn;
129
+ }).filter(item => item !== null);
130
+ resolved[key] = resolvedArray;
131
+ }
132
+ // Recursão para objetos aninhados
133
+ else if (value && typeof value === 'object') {
134
+ resolved[key] = resolveObject(value);
135
+ }
136
+ // Valores primitivos
137
+ else {
138
+ resolved[key] = value;
139
+ }
140
+ });
141
+ return resolved;
142
+ }
143
+ return obj;
144
+ }
145
+ return resolveObject(data);
146
+ }
147
+ // Função para extrair dados com resolução automática de referências
148
+ function extractDataWithReferences(elements, included, fieldsMap) {
149
+ // Filtrar dados pelos elementos
150
+ const filteredData = included.filter(item => elements.includes(item.entityUrn));
151
+ // Resolver todas as referências
152
+ const resolvedData = filteredData.map(item => resolveReferences(item, included));
153
+ // Se há mapeamento de campos, aplicar
154
+ if (fieldsMap) {
155
+ return extractFields(resolvedData, fieldsMap);
156
+ }
157
+ return resolvedData;
158
+ }
159
+ // Função para debug de estrutura com referências resolvidas
160
+ function debugResolvedStructure(elements, included, maxDepth = 2) {
161
+ console.log('🔍 Estrutura dos dados com referências resolvidas:');
162
+ const resolved = extractDataWithReferences(elements, included);
163
+ if (resolved.length > 0) {
164
+ console.log(`📊 Total de itens: ${resolved.length}`);
165
+ console.log('📋 Estrutura do primeiro item:');
166
+ debugObjectStructure(resolved[0], maxDepth);
167
+ }
168
+ }
169
+ // Função para extrair campos específicos de todos os objetos no included
170
+ function extractFieldsFromIncluded(included, fields) {
171
+ return included
172
+ .filter(item => fields.some(field => item[field] !== undefined))
173
+ .map(item => {
174
+ const extracted = { entityUrn: item.entityUrn };
175
+ fields.forEach(field => {
176
+ if (item[field] !== undefined) {
177
+ extracted[field] = item[field];
178
+ }
179
+ });
180
+ return extracted;
181
+ });
182
+ }
183
+ // Função para associar dados extras aos dados principais
184
+ function mergeExtraFields(mainData, extraData, matchKey = 'companyUrn') {
185
+ return mainData.map(item => {
186
+ const extraItem = extraData.find(extra => item[matchKey] && extra.entityUrn === item[matchKey]);
187
+ if (extraItem) {
188
+ const { entityUrn } = extraItem, extraFields = __rest(extraItem, ["entityUrn"]);
189
+ return Object.assign(Object.assign({}, item), extraFields);
190
+ }
191
+ return item;
192
+ });
193
+ }
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "linkedin-api-voyager",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
+ "description": "Uma biblioteca TypeScript para interagir com a API interna do LinkedIn (Voyager)",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
4
7
  "license": "MIT",
8
+ "files": [
9
+ "lib/**/*"
10
+ ],
5
11
  "scripts": {
6
12
  "build": "tsc",
7
13
  "prepare": "npm run build",
8
14
  "dev": "nodemon src/index.ts",
9
- "publish": "yarn build && npm publish"
15
+ "prepublishOnly": "npm run build"
10
16
  },
11
17
  "devDependencies": {
12
18
  "@types/fs-extra": "^11.0.4",
package/src/account.ts DELETED
@@ -1,116 +0,0 @@
1
- import { fetchData } from "./config";
2
- import {
3
- extractDataWithReferences,
4
- extractFields,
5
- extractFieldsFromIncluded,
6
- mergeExtraFields,
7
- } from "./utils";
8
-
9
- export const getProfile = async (identifier: string) => {
10
- const response = await fetchData(
11
- `/identity/profiles/${identifier}/profileView`
12
- );
13
-
14
- const data = response.data;
15
- const dataResult: any[] = response?.included;
16
-
17
- const getEntityByUrn = (urn: string) =>
18
- dataResult.find((item) => item.entityUrn === urn);
19
-
20
- const keyProfile = getEntityByUrn(data?.["*profile"]);
21
- if (!keyProfile) throw new Error("Key profile not found");
22
-
23
- const miniProfile = getEntityByUrn(keyProfile?.["*miniProfile"]);
24
- if (!miniProfile) throw new Error("Mini profile not found");
25
-
26
- const profile = {
27
- // id_urn: keyProfile.entityUrn?.split("urn:li:fs_profile:")[1] || null,
28
- publicIdentifier: miniProfile?.publicIdentifier || null,
29
- firstName: keyProfile.firstName || null,
30
- lastName: keyProfile.lastName || null,
31
- fullName: `${keyProfile.firstName || ""} ${keyProfile.lastName || ""}`,
32
- birthDate: keyProfile.birthDate
33
- ? JSON.stringify({
34
- month: keyProfile.birthDate.month,
35
- day: keyProfile.birthDate.day,
36
- })
37
- : null,
38
- profilePicture: miniProfile.picture
39
- ? `${miniProfile.picture.rootUrl}${
40
- miniProfile.picture.artifacts[
41
- miniProfile.picture.artifacts.length - 1
42
- ]?.fileIdentifyingUrlPathSegment
43
- }`
44
- : null,
45
- backgroundPicture: miniProfile.backgroundImage
46
- ? `${miniProfile.backgroundImage.rootUrl}${
47
- miniProfile.backgroundImage.artifacts[
48
- miniProfile.backgroundImage.artifacts.length - 1
49
- ]?.fileIdentifyingUrlPathSegment
50
- }`
51
- : null,
52
- location: {
53
- country: keyProfile.locationName || null,
54
- city: keyProfile.geoLocationName || null,
55
- },
56
- address: keyProfile.address || null,
57
- industry: keyProfile.industryName || null,
58
- headline: keyProfile.headline || null,
59
- summary: keyProfile.summary || null,
60
- };
61
-
62
- return profile;
63
- };
64
-
65
- export const getProfissionalExperiences = async (identifier: string) => {
66
- const response = await fetchData(
67
- `/identity/profiles/${identifier}/positions`
68
- );
69
-
70
- let { data, included } = response;
71
- const elements = data["*elements"] as string[];
72
-
73
- // Usar a nova função para resolver referências automaticamente
74
- const dataExperiences = extractDataWithReferences(elements, included);
75
-
76
- // Extrair campos específicos do included
77
- const extraFields = extractFieldsFromIncluded(included, ["universalName"]);
78
-
79
- // Mapeamento de campos
80
- const fieldsMap = {
81
- id: "entityUrn",
82
- title: "title",
83
- companyName: "company.miniCompany.name",
84
- companyUrn: "companyUrn",
85
- companyEmployeeCount: "company.employeeCountRange",
86
- companyIndustries: "company.miniCompany.industries",
87
- description: "description",
88
- location: "locationName",
89
- geoLocation: "geoLocationName",
90
- timePeriod: "timePeriod",
91
- startDate: "timePeriod.startDate",
92
- endDate: "timePeriod.endDate",
93
- };
94
-
95
- // Aplicar mapeamento aos dados resolvidos
96
- const mappedExperiences = extractFields(dataExperiences, fieldsMap);
97
-
98
- // Associar campos extras
99
- const experiencesWithExtras = mergeExtraFields(
100
- mappedExperiences,
101
- extraFields,
102
- "companyUrn"
103
- );
104
-
105
- // Ordenar: sem endDate (ativo) primeiro, depois do mais recente ao mais antigo
106
- return experiencesWithExtras.sort((a, b) => {
107
- if (!a.endDate && b.endDate) return -1;
108
- if (a.endDate && !b.endDate) return 1;
109
- if (!a.endDate && !b.endDate) return 0;
110
-
111
- const yearDiff = (b.endDate.year || 0) - (a.endDate.year || 0);
112
- if (yearDiff !== 0) return yearDiff;
113
-
114
- return (b.endDate.month || 0) - (a.endDate.month || 0);
115
- });
116
- };
package/src/company.ts DELETED
@@ -1,33 +0,0 @@
1
- import { fetchData } from "./config";
2
- import { extractDataWithReferences, extractFields } from "./utils";
3
-
4
- export const getCompany = async (identifier: string) => {
5
- const response = await fetchData(
6
- `/organization/companies?decorationId=com.linkedin.voyager.deco.organization.web.WebFullCompanyMain-12&q=universalName&universalName=${identifier}`
7
- );
8
-
9
- const data = extractDataWithReferences(
10
- response.data["*elements"],
11
- response.included
12
- );
13
-
14
- const fieldsMap = {
15
- id: "entityUrn",
16
- name: "name",
17
- description: "description",
18
- username: "universalName",
19
- companyPageUrl: "companyPageUrl",
20
- staffCount: "staffCount",
21
- url: "url",
22
- companyIndustries: "*companyIndustries[0].localizedName",
23
- location: "locationName",
24
- jobSearchPageUrl: "jobSearchPageUrl",
25
- phone: "phone",
26
- followerCount: "followingInfo.followerCount",
27
- backgroundCoverImage: "backgroundCoverImage.image",
28
- logo: "logo.image",
29
- permissions: "permissions",
30
- };
31
-
32
- return extractFields(data, fieldsMap)[0];
33
- };
package/src/config.ts DELETED
@@ -1,109 +0,0 @@
1
- import * as path from "path";
2
- import * as os from "os";
3
- import * as fs from "fs-extra";
4
- import axios from "axios";
5
-
6
- export const COOKIE_FILE_PATH = "linkedin_cookies.json";
7
-
8
- export const API_BASE_URL = "https://www.linkedin.com/voyager/api";
9
- export const AUTH_BASE_URL = "https://www.linkedin.com";
10
-
11
- // Interface para os cookies
12
- interface LinkedInCookies {
13
- JSESSIONID: string;
14
- li_at: string;
15
- timestamp: number;
16
- }
17
-
18
- // Função para salvar cookies no arquivo JSON
19
- export const saveCookies = async (
20
- JSESSIONID: string,
21
- li_at: string
22
- ): Promise<void> => {
23
- try {
24
- const cookies: LinkedInCookies = {
25
- JSESSIONID,
26
- li_at,
27
- timestamp: Date.now(),
28
- };
29
-
30
- await fs.ensureFile(COOKIE_FILE_PATH);
31
- await fs.writeJson(COOKIE_FILE_PATH, cookies, { spaces: 2 });
32
- console.log(`Cookies salvos em: ${COOKIE_FILE_PATH}`);
33
- } catch (error) {
34
- console.error("Erro ao salvar cookies:", error);
35
- throw error;
36
- }
37
- };
38
-
39
- // Função para carregar cookies do arquivo JSON
40
- export const loadCookies = async (): Promise<LinkedInCookies | null> => {
41
- try {
42
- const exists = await fs.pathExists(COOKIE_FILE_PATH);
43
- if (!exists) {
44
- console.log("Arquivo de cookies não encontrado");
45
- return null;
46
- }
47
-
48
- const cookies = await fs.readJson(COOKIE_FILE_PATH);
49
-
50
- // Verificar se os cookies têm a estrutura esperada
51
- if (!cookies.JSESSIONID || !cookies.li_at) {
52
- console.log("Cookies inválidos encontrados no arquivo");
53
- return null;
54
- }
55
-
56
- console.log(`Cookies carregados de: ${COOKIE_FILE_PATH}`);
57
- return cookies;
58
- } catch (error) {
59
- console.error("Erro ao carregar cookies:", error);
60
- return null;
61
- }
62
- };
63
-
64
- // Função para criar cliente com cookies automáticos
65
- export const Client = async (providedCookies?: {
66
- JSESSIONID: string;
67
- li_at: string;
68
- }): Promise<ReturnType<typeof api>> => {
69
- let cookiesToUse: { JSESSIONID: string; li_at: string };
70
- const savedCookies = await loadCookies();
71
-
72
- if (savedCookies) {
73
- cookiesToUse = {
74
- JSESSIONID: savedCookies.JSESSIONID,
75
- li_at: savedCookies.li_at,
76
- };
77
- } else {
78
- if (providedCookies) {
79
- await saveCookies(providedCookies.JSESSIONID, providedCookies.li_at);
80
- cookiesToUse = providedCookies;
81
- } else {
82
- throw new Error("Nenhum cookie válido fornecido");
83
- }
84
- }
85
-
86
- return api({
87
- JSESSIONID: parseInt(cookiesToUse.JSESSIONID),
88
- li_at: cookiesToUse.li_at,
89
- });
90
- };
91
-
92
- const api = ({ JSESSIONID, li_at }: { li_at: string; JSESSIONID: number }) => {
93
- return axios.create({
94
- baseURL: API_BASE_URL,
95
- headers: {
96
- "accept-language":
97
- "pt-BR,pt;q=0.9,fr-FR;q=0.8,fr;q=0.7,en-US;q=0.6,en;q=0.5",
98
- accept: "application/vnd.linkedin.normalized+json+2.1",
99
- cookie: `li_at=${li_at}; JSESSIONID="ajax:${JSESSIONID}"`,
100
- "csrf-token": `ajax:${JSESSIONID}`,
101
- },
102
- });
103
- };
104
-
105
- export const fetchData = async (endpoint: string) => {
106
- const api = await Client();
107
- const response = await api.get(endpoint);
108
- return response.data;
109
- };
package/src/posts.ts DELETED
@@ -1,71 +0,0 @@
1
- import { fetchData } from "./config";
2
- import { extractFields } from "./utils";
3
-
4
- export const getCommentsByPostUrl = async (
5
- url: string,
6
- start: number = 0,
7
- limit: number = 50,
8
- accumulatedComments: any[] = []
9
- ): Promise<any[]> => {
10
- const postID = url.match(/activity-(\d+)/)?.[1];
11
-
12
- const response = await fetchData(
13
- `/graphql?includeWebMetadata=false&queryId=voyagerSocialDashComments.95ed44bc87596acce7c460c70934d0ff&variables=(count:${limit},start:${start},numReplies:1,socialDetailUrn:urn%3Ali%3Afsd_socialDetail%3A%28urn%3Ali%3Aactivity%${postID}%2Curn%3Ali%3Aactivity%3A${postID}%2Curn%3Ali%3AhighlightedReply%3A-%29,sortOrder:RELEVANCE)`
14
- );
15
-
16
- const elements = response.data?.data?.socialDashCommentsBySocialDetail?.[
17
- "*elements"
18
- ] as string[];
19
-
20
- // Se não há elementos, retorna os comentários acumulados
21
- if (!elements || elements.length === 0) {
22
- // console.log(
23
- // "✅ Busca finalizada. Total de comentários:",
24
- // accumulatedComments.length
25
- // );
26
- return accumulatedComments;
27
- }
28
-
29
- const data =
30
- response.included?.filter((item: any) =>
31
- elements.includes(item.entityUrn)
32
- ) || [];
33
-
34
- // Mapeamento melhorado dos campos
35
- const fieldsMap = {
36
- id: "entityUrn",
37
- createdAt: "createdAt",
38
- isAuthor: "commenter.author",
39
- name: "commenter.title.text",
40
- headline: "commenter.subtitle",
41
- profileUrl: "commenter.navigationUrl",
42
- comment: "commentary.text",
43
- permalink: "permalink",
44
- image:
45
- "commenter.image.attributes.0.detailData.nonEntityProfilePicture.vectorImage",
46
- };
47
-
48
- const currentComments = extractFields(data, fieldsMap);
49
- const allComments = [...accumulatedComments, ...currentComments];
50
-
51
- // console.log(
52
- // `🔍 Encontrados ${elements.length} comentários (Total: ${allComments.length})`
53
- // );
54
-
55
- // Continua a busca se há mais elementos
56
- if (elements.length > 0) {
57
- return await getCommentsByPostUrl(
58
- url,
59
- start + elements.length,
60
- limit,
61
- allComments
62
- );
63
- }
64
-
65
- // Se retornou menos que o limite, chegou ao fim
66
- return allComments;
67
- };
68
-
69
- export const getPosts = async () => {
70
- return [];
71
- };