linkedin-api-voyager 1.3.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/README.md +197 -0
- package/package.json +22 -0
- package/src/account.ts +116 -0
- package/src/company.ts +33 -0
- package/src/config.ts +109 -0
- package/src/index.ts +6 -0
- package/src/linkedin.ts +0 -0
- package/src/posts.ts +71 -0
- package/src/search.ts +183 -0
- package/src/utils.ts +213 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# LinkedIn API Voyager
|
|
2
|
+
|
|
3
|
+
Uma biblioteca TypeScript para interagir com a API interna do LinkedIn (Voyager) de forma simples e eficiente.
|
|
4
|
+
|
|
5
|
+
## 🚀 Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install linkedin-api-voyager
|
|
9
|
+
# ou
|
|
10
|
+
yarn add linkedin-api-voyager
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 📋 Pré-requisitos
|
|
14
|
+
|
|
15
|
+
Você precisa ter cookies válidos do LinkedIn para usar esta biblioteca. Os cookies devem estar salvos em um arquivo `linkedin_cookies.json` na raiz do seu projeto.
|
|
16
|
+
|
|
17
|
+
### Formato dos cookies:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"JSESSIONID": "seu_token_aqui",
|
|
22
|
+
"li_at": "seu_token_aqui",
|
|
23
|
+
"timestamp": "1234567890"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 🔧 Uso
|
|
28
|
+
|
|
29
|
+
### Configuração inicial
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
getProfile,
|
|
34
|
+
getProfissionalExperiences,
|
|
35
|
+
getCompany,
|
|
36
|
+
search,
|
|
37
|
+
} from "linkedin-api-voyager";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 👤 Perfil de usuário
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Obter perfil completo
|
|
44
|
+
const profile = await getProfile("username-do-linkedin");
|
|
45
|
+
|
|
46
|
+
// Obter experiências profissionais (ordenadas do mais recente ao mais antigo)
|
|
47
|
+
const experiences = await getProfissionalExperiences("username-do-linkedin");
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 🏢 Informações de empresa
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Obter dados completos da empresa
|
|
54
|
+
const company = await getCompany("nome-universal-da-empresa");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 🔍 Busca
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Busca geral
|
|
61
|
+
const results = await search(
|
|
62
|
+
{
|
|
63
|
+
keywords: "desenvolvedor javascript",
|
|
64
|
+
filters: "List(resultType->PEOPLE)",
|
|
65
|
+
},
|
|
66
|
+
{ limit: 50 }
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Busca com parâmetros personalizados
|
|
70
|
+
const customSearch = await search({
|
|
71
|
+
q: "all",
|
|
72
|
+
keywords: "react developer",
|
|
73
|
+
filters: "List(resultType->PEOPLE,locationFilter->br:0)",
|
|
74
|
+
start: 0,
|
|
75
|
+
count: "25",
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 💬 Comentários de posts
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { getCommentsByPostUrl } from "linkedin-api-voyager";
|
|
83
|
+
|
|
84
|
+
// Obter todos os comentários de um post
|
|
85
|
+
const comments = await getCommentsByPostUrl(
|
|
86
|
+
"https://www.linkedin.com/feed/update/urn:li:activity-1234567890/",
|
|
87
|
+
0, // início
|
|
88
|
+
50 // limite por página
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 📊 Estrutura de dados
|
|
93
|
+
|
|
94
|
+
### Perfil
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
{
|
|
98
|
+
publicIdentifier: string;
|
|
99
|
+
firstName: string;
|
|
100
|
+
lastName: string;
|
|
101
|
+
fullName: string;
|
|
102
|
+
profilePicture: string;
|
|
103
|
+
backgroundPicture: string;
|
|
104
|
+
location: {
|
|
105
|
+
country: string;
|
|
106
|
+
city: string;
|
|
107
|
+
}
|
|
108
|
+
industry: string;
|
|
109
|
+
headline: string;
|
|
110
|
+
summary: string;
|
|
111
|
+
// ... outros campos
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Experiências profissionais
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
id: string;
|
|
121
|
+
title: string;
|
|
122
|
+
companyName: string;
|
|
123
|
+
companyUrn: string;
|
|
124
|
+
universalName: string; // Nome universal da empresa
|
|
125
|
+
description: string;
|
|
126
|
+
location: string;
|
|
127
|
+
startDate: { year: number; month: number };
|
|
128
|
+
endDate: { year: number; month: number } | null; // null = ativo
|
|
129
|
+
// ... outros campos
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Empresa
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
{
|
|
138
|
+
id: string;
|
|
139
|
+
name: string;
|
|
140
|
+
description: string;
|
|
141
|
+
username: string;
|
|
142
|
+
companyPageUrl: string;
|
|
143
|
+
staffCount: number;
|
|
144
|
+
url: string;
|
|
145
|
+
location: string;
|
|
146
|
+
followerCount: number;
|
|
147
|
+
logo: object;
|
|
148
|
+
// ... outros campos
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 🛠️ Funcionalidades avançadas
|
|
153
|
+
|
|
154
|
+
### Extração de campos personalizados
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { extractFields, extractFieldsFromIncluded } from "linkedin-api-voyager";
|
|
158
|
+
|
|
159
|
+
// Mapear campos específicos
|
|
160
|
+
const fieldsMap = {
|
|
161
|
+
nome: "firstName",
|
|
162
|
+
empresa: "company.name",
|
|
163
|
+
cargo: "title",
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const dadosMapeados = extractFields(dados, fieldsMap);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Resolução automática de referências
|
|
170
|
+
|
|
171
|
+
A biblioteca resolve automaticamente referências URN aninhadas, permitindo acesso direto a dados relacionados sem necessidade de mapeamento manual.
|
|
172
|
+
|
|
173
|
+
## ⚠️ Limitações e considerações
|
|
174
|
+
|
|
175
|
+
- Esta biblioteca usa a API interna do LinkedIn (Voyager)
|
|
176
|
+
- Requer cookies válidos de uma sessão autenticada
|
|
177
|
+
- Respeite os termos de uso do LinkedIn
|
|
178
|
+
- Use com moderação para evitar bloqueios
|
|
179
|
+
- Não é uma API oficial do LinkedIn
|
|
180
|
+
|
|
181
|
+
## 🔒 Segurança
|
|
182
|
+
|
|
183
|
+
- Mantenha seus cookies seguros
|
|
184
|
+
- Não compartilhe o arquivo `linkedin_cookies.json`
|
|
185
|
+
- O arquivo já está incluído no `.gitignore`
|
|
186
|
+
|
|
187
|
+
## 📝 Licença
|
|
188
|
+
|
|
189
|
+
MIT
|
|
190
|
+
|
|
191
|
+
## 🤝 Contribuição
|
|
192
|
+
|
|
193
|
+
Contribuições são bem-vindas! Por favor, abra uma issue ou pull request.
|
|
194
|
+
|
|
195
|
+
## 📞 Suporte
|
|
196
|
+
|
|
197
|
+
Para dúvidas ou problemas, abra uma issue no repositório.
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linkedin-api-voyager",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"prepare": "npm run build",
|
|
8
|
+
"dev": "nodemon src/index.ts",
|
|
9
|
+
"publish": "yarn build && npm publish"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/fs-extra": "^11.0.4",
|
|
13
|
+
"@types/node": "^22.13.1",
|
|
14
|
+
"nodemon": "^3.1.9",
|
|
15
|
+
"ts-node": "^10.9.2",
|
|
16
|
+
"typescript": "^5.7.3"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"axios": "^1.11.0",
|
|
20
|
+
"fs-extra": "^11.3.1"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/account.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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/index.ts
ADDED
package/src/linkedin.ts
ADDED
|
File without changes
|
package/src/posts.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
};
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Client as ClientState } from "./config";
|
|
2
|
+
|
|
3
|
+
// Constantes
|
|
4
|
+
const MAX_SEARCH_COUNT = 25;
|
|
5
|
+
const MAX_REPEATED_REQUESTS = 40;
|
|
6
|
+
|
|
7
|
+
// Interfaces
|
|
8
|
+
export interface SearchParams {
|
|
9
|
+
count?: string;
|
|
10
|
+
filters?: string;
|
|
11
|
+
origin?: string;
|
|
12
|
+
q?: string;
|
|
13
|
+
start?: number;
|
|
14
|
+
queryContext?: string;
|
|
15
|
+
[key: string]: any; // Para permitir parâmetros adicionais
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SearchElement {
|
|
19
|
+
[key: string]: any; // Estrutura flexível para elementos de busca
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SearchDataElement {
|
|
23
|
+
elements: SearchElement[];
|
|
24
|
+
extendedElements?: SearchElement[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SearchResponse {
|
|
28
|
+
data: {
|
|
29
|
+
elements: SearchDataElement[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SearchOptions {
|
|
34
|
+
limit?: number;
|
|
35
|
+
results?: SearchElement[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Função utilitária para criar query string
|
|
39
|
+
const createQueryString = (params: Record<string, any>): string => {
|
|
40
|
+
return Object.entries(params)
|
|
41
|
+
.map(
|
|
42
|
+
([key, value]) =>
|
|
43
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
44
|
+
)
|
|
45
|
+
.join("&");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Função utilitária para fazer fetch (assumindo que existe uma função _fetch no client)
|
|
49
|
+
const fetchData = async (endpoint: string): Promise<SearchResponse> => {
|
|
50
|
+
const api = await ClientState();
|
|
51
|
+
const response = await api.get(endpoint);
|
|
52
|
+
return response.data;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Função principal de busca
|
|
56
|
+
export const search = async (
|
|
57
|
+
params: SearchParams,
|
|
58
|
+
options: SearchOptions = {}
|
|
59
|
+
): Promise<SearchElement[]> => {
|
|
60
|
+
const { limit, results = [] } = options;
|
|
61
|
+
|
|
62
|
+
// Determinar o count baseado no limite
|
|
63
|
+
const count = limit && limit <= MAX_SEARCH_COUNT ? limit : MAX_SEARCH_COUNT;
|
|
64
|
+
|
|
65
|
+
// Parâmetros padrão
|
|
66
|
+
const defaultParams: SearchParams = {
|
|
67
|
+
count: count.toString(),
|
|
68
|
+
filters: "List()",
|
|
69
|
+
origin: "GLOBAL_SEARCH_HEADER",
|
|
70
|
+
q: "all",
|
|
71
|
+
start: results.length,
|
|
72
|
+
queryContext:
|
|
73
|
+
"List(spellCorrectionEnabled->true,relatedSearchesEnabled->true,kcardTypes->PROFILE|COMPANY)",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Mesclar parâmetros padrão com os fornecidos
|
|
77
|
+
const mergedParams = { ...defaultParams, ...params };
|
|
78
|
+
|
|
79
|
+
// Fazer a requisição
|
|
80
|
+
const endpoint = `/search/blended?${createQueryString(mergedParams)}`;
|
|
81
|
+
|
|
82
|
+
const response = await fetchData(endpoint);
|
|
83
|
+
|
|
84
|
+
// Processar os dados da resposta
|
|
85
|
+
const newElements: SearchElement[] = [];
|
|
86
|
+
|
|
87
|
+
if (response.data && response.data.elements) {
|
|
88
|
+
for (let i = 0; i < response.data.elements.length; i++) {
|
|
89
|
+
if (response.data.elements[i].elements) {
|
|
90
|
+
newElements.push(...response.data.elements[i].elements);
|
|
91
|
+
}
|
|
92
|
+
// Comentário: não tenho certeza do que extendedElements geralmente se refere
|
|
93
|
+
// - busca por palavra-chave retorna um único trabalho?
|
|
94
|
+
// if (response.data.elements[i].extendedElements) {
|
|
95
|
+
// newElements.push(...response.data.elements[i].extendedElements);
|
|
96
|
+
// }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Adicionar novos elementos aos resultados
|
|
101
|
+
const updatedResults = [...results, ...newElements];
|
|
102
|
+
|
|
103
|
+
// Sempre cortar os resultados, não importa o que a requisição retorna
|
|
104
|
+
const trimmedResults = limit
|
|
105
|
+
? updatedResults.slice(0, limit)
|
|
106
|
+
: updatedResults;
|
|
107
|
+
|
|
108
|
+
// Caso base da recursão
|
|
109
|
+
const shouldStop =
|
|
110
|
+
(limit !== undefined &&
|
|
111
|
+
(trimmedResults.length >= limit || // se nossos resultados excedem o limite definido
|
|
112
|
+
trimmedResults.length / count >= MAX_REPEATED_REQUESTS)) ||
|
|
113
|
+
newElements.length === 0;
|
|
114
|
+
|
|
115
|
+
if (shouldStop) {
|
|
116
|
+
return trimmedResults;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Chamada recursiva
|
|
120
|
+
return search(params, {
|
|
121
|
+
limit,
|
|
122
|
+
results: trimmedResults,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Função auxiliar para busca simples (não recursiva)
|
|
127
|
+
// export const searchSingle = async (
|
|
128
|
+
// client: ClientState,
|
|
129
|
+
// params: SearchParams
|
|
130
|
+
// ): Promise<SearchElement[]> => {
|
|
131
|
+
// return search(client, params, { limit: MAX_SEARCH_COUNT });
|
|
132
|
+
// };
|
|
133
|
+
|
|
134
|
+
// // Função para busca com limite específico
|
|
135
|
+
// export const searchWithLimit = async (
|
|
136
|
+
// client: ClientState,
|
|
137
|
+
// params: SearchParams,
|
|
138
|
+
// limit: number
|
|
139
|
+
// ): Promise<SearchElement[]> => {
|
|
140
|
+
// return search(client, params, { limit });
|
|
141
|
+
// };
|
|
142
|
+
|
|
143
|
+
// // Função para busca de pessoas
|
|
144
|
+
// export const searchPeople = async (
|
|
145
|
+
// client: ClientState,
|
|
146
|
+
// query: string,
|
|
147
|
+
// limit?: number
|
|
148
|
+
// ): Promise<SearchElement[]> => {
|
|
149
|
+
// const params: SearchParams = {
|
|
150
|
+
// keywords: query,
|
|
151
|
+
// filters: "List(resultType->PEOPLE)",
|
|
152
|
+
// };
|
|
153
|
+
|
|
154
|
+
// return search(client, params, { limit });
|
|
155
|
+
// };
|
|
156
|
+
|
|
157
|
+
// // Função para busca de empresas
|
|
158
|
+
// export const searchCompanies = async (
|
|
159
|
+
// client: ClientState,
|
|
160
|
+
// query: string,
|
|
161
|
+
// limit?: number
|
|
162
|
+
// ): Promise<SearchElement[]> => {
|
|
163
|
+
// const params: SearchParams = {
|
|
164
|
+
// keywords: query,
|
|
165
|
+
// filters: "List(resultType->COMPANIES)",
|
|
166
|
+
// };
|
|
167
|
+
|
|
168
|
+
// return search(client, params, { limit });
|
|
169
|
+
// };
|
|
170
|
+
|
|
171
|
+
// // Função para busca de empregos
|
|
172
|
+
// export const searchJobs = async (
|
|
173
|
+
// client: ClientState,
|
|
174
|
+
// query: string,
|
|
175
|
+
// limit?: number
|
|
176
|
+
// ): Promise<SearchElement[]> => {
|
|
177
|
+
// const params: SearchParams = {
|
|
178
|
+
// keywords: query,
|
|
179
|
+
// filters: "List(resultType->JOBS)",
|
|
180
|
+
// };
|
|
181
|
+
|
|
182
|
+
// return search(client, params, { limit });
|
|
183
|
+
// };
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export function filterKeys(obj: any, keysToKeep: string[]) {
|
|
2
|
+
const filteredObject: any = {};
|
|
3
|
+
keysToKeep.forEach((key) => {
|
|
4
|
+
if (obj.hasOwnProperty(key)) {
|
|
5
|
+
filteredObject[key] = obj[key];
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
return filteredObject;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function filterOutKeys(obj: any, keysToIgnore: string[]) {
|
|
12
|
+
const filteredObject: any = {};
|
|
13
|
+
Object.keys(obj).forEach((key) => {
|
|
14
|
+
if (!keysToIgnore.includes(key)) {
|
|
15
|
+
filteredObject[key] = obj[key];
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return filteredObject;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Nova função para extrair valores de caminhos aninhados
|
|
22
|
+
export function getNestedValue(obj: any, path: string): any {
|
|
23
|
+
return path.split('.').reduce((current, key) => {
|
|
24
|
+
// Lidar com arrays como attributes[0]
|
|
25
|
+
if (key.includes('[') && key.includes(']')) {
|
|
26
|
+
const [arrayKey, indexStr] = key.split('[');
|
|
27
|
+
const index = parseInt(indexStr.replace(']', ''));
|
|
28
|
+
return current?.[arrayKey]?.[index];
|
|
29
|
+
}
|
|
30
|
+
return current?.[key];
|
|
31
|
+
}, obj);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Nova função melhorada para filtrar com caminhos aninhados
|
|
35
|
+
export function extractFields(data: any[], fieldsMap: Record<string, string>): any[] {
|
|
36
|
+
return data.map(item => {
|
|
37
|
+
const extracted: any = {};
|
|
38
|
+
|
|
39
|
+
Object.entries(fieldsMap).forEach(([newKey, path]) => {
|
|
40
|
+
const value = getNestedValue(item, path);
|
|
41
|
+
if (value !== undefined) {
|
|
42
|
+
extracted[newKey] = value;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return extracted;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Função para debug - mostra a estrutura do objeto
|
|
51
|
+
export function debugObjectStructure(obj: any, maxDepth: number = 3, currentDepth: number = 0): void {
|
|
52
|
+
if (currentDepth >= maxDepth) return;
|
|
53
|
+
|
|
54
|
+
const indent = ' '.repeat(currentDepth);
|
|
55
|
+
|
|
56
|
+
if (Array.isArray(obj)) {
|
|
57
|
+
console.log(`${indent}Array[${obj.length}]:`);
|
|
58
|
+
if (obj.length > 0) {
|
|
59
|
+
console.log(`${indent} [0]:`);
|
|
60
|
+
debugObjectStructure(obj[0], maxDepth, currentDepth + 2);
|
|
61
|
+
}
|
|
62
|
+
} else if (obj && typeof obj === 'object') {
|
|
63
|
+
Object.keys(obj).slice(0, 10).forEach(key => {
|
|
64
|
+
const value = obj[key];
|
|
65
|
+
if (typeof value === 'object' && value !== null) {
|
|
66
|
+
console.log(`${indent}${key}:`);
|
|
67
|
+
debugObjectStructure(value, maxDepth, currentDepth + 1);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`${indent}${key}: ${typeof value} = ${String(value).slice(0, 50)}...`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Função para resolver referências URN dinamicamente
|
|
76
|
+
export function resolveReferences(data: any, included: any[]): any {
|
|
77
|
+
if (!data || !included) return data;
|
|
78
|
+
|
|
79
|
+
// Criar um mapa de URN para acesso rápido
|
|
80
|
+
const urnMap = new Map();
|
|
81
|
+
included.forEach(item => {
|
|
82
|
+
if (item.entityUrn) {
|
|
83
|
+
urnMap.set(item.entityUrn, item);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Função recursiva para resolver referências
|
|
88
|
+
function resolveObject(obj: any): any {
|
|
89
|
+
if (Array.isArray(obj)) {
|
|
90
|
+
return obj.map(item => resolveObject(item));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (obj && typeof obj === 'object') {
|
|
94
|
+
const resolved: any = {};
|
|
95
|
+
|
|
96
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
97
|
+
// Detectar chaves que começam com * (referências URN)
|
|
98
|
+
if (key.startsWith('*') && typeof value === 'string') {
|
|
99
|
+
const referencedData = urnMap.get(value);
|
|
100
|
+
if (referencedData) {
|
|
101
|
+
// Remover o * e usar como chave
|
|
102
|
+
const cleanKey = key.substring(1);
|
|
103
|
+
resolved[cleanKey] = resolveObject(referencedData);
|
|
104
|
+
} else {
|
|
105
|
+
resolved[key] = value; // Manter original se não encontrar
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Detectar arrays de URNs
|
|
109
|
+
else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string' && value[0].startsWith('urn:li:')) {
|
|
110
|
+
const resolvedArray = value.map(urn => {
|
|
111
|
+
const referencedData = urnMap.get(urn);
|
|
112
|
+
return referencedData ? resolveObject(referencedData) : urn;
|
|
113
|
+
}).filter(item => item !== null);
|
|
114
|
+
resolved[key] = resolvedArray;
|
|
115
|
+
}
|
|
116
|
+
// Recursão para objetos aninhados
|
|
117
|
+
else if (value && typeof value === 'object') {
|
|
118
|
+
resolved[key] = resolveObject(value);
|
|
119
|
+
}
|
|
120
|
+
// Valores primitivos
|
|
121
|
+
else {
|
|
122
|
+
resolved[key] = value;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return obj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return resolveObject(data);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Função para extrair dados com resolução automática de referências
|
|
136
|
+
export function extractDataWithReferences(
|
|
137
|
+
elements: string[],
|
|
138
|
+
included: any[],
|
|
139
|
+
fieldsMap?: Record<string, string>
|
|
140
|
+
): any[] {
|
|
141
|
+
// Filtrar dados pelos elementos
|
|
142
|
+
const filteredData = included.filter(item =>
|
|
143
|
+
elements.includes(item.entityUrn)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Resolver todas as referências
|
|
147
|
+
const resolvedData = filteredData.map(item =>
|
|
148
|
+
resolveReferences(item, included)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Se há mapeamento de campos, aplicar
|
|
152
|
+
if (fieldsMap) {
|
|
153
|
+
return extractFields(resolvedData, fieldsMap);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resolvedData;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Função para debug de estrutura com referências resolvidas
|
|
160
|
+
export function debugResolvedStructure(
|
|
161
|
+
elements: string[],
|
|
162
|
+
included: any[],
|
|
163
|
+
maxDepth: number = 2
|
|
164
|
+
): void {
|
|
165
|
+
console.log('🔍 Estrutura dos dados com referências resolvidas:');
|
|
166
|
+
const resolved = extractDataWithReferences(elements, included);
|
|
167
|
+
|
|
168
|
+
if (resolved.length > 0) {
|
|
169
|
+
console.log(`📊 Total de itens: ${resolved.length}`);
|
|
170
|
+
console.log('📋 Estrutura do primeiro item:');
|
|
171
|
+
debugObjectStructure(resolved[0], maxDepth);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Função para extrair campos específicos de todos os objetos no included
|
|
176
|
+
export function extractFieldsFromIncluded(
|
|
177
|
+
included: any[],
|
|
178
|
+
fields: string[]
|
|
179
|
+
): Record<string, any>[] {
|
|
180
|
+
return included
|
|
181
|
+
.filter(item => fields.some(field => item[field] !== undefined))
|
|
182
|
+
.map(item => {
|
|
183
|
+
const extracted: any = { entityUrn: item.entityUrn };
|
|
184
|
+
|
|
185
|
+
fields.forEach(field => {
|
|
186
|
+
if (item[field] !== undefined) {
|
|
187
|
+
extracted[field] = item[field];
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return extracted;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Função para associar dados extras aos dados principais
|
|
196
|
+
export function mergeExtraFields(
|
|
197
|
+
mainData: any[],
|
|
198
|
+
extraData: Record<string, any>[],
|
|
199
|
+
matchKey: string = 'companyUrn'
|
|
200
|
+
): any[] {
|
|
201
|
+
return mainData.map(item => {
|
|
202
|
+
const extraItem = extraData.find(extra =>
|
|
203
|
+
item[matchKey] && extra.entityUrn === item[matchKey]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (extraItem) {
|
|
207
|
+
const { entityUrn, ...extraFields } = extraItem;
|
|
208
|
+
return { ...item, ...extraFields };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return item;
|
|
212
|
+
});
|
|
213
|
+
}
|