six-degrees 0.4.9 → 0.4.11
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__05_3-22._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +4 -4
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +7 -7
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/standalone/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/standalone/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/standalone/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{static/chunks/1z_rt4jteewqp.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/.next/standalone/CHANGELOG.md +57 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/scraper/route.js +5 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/GalaxyLab.js +4 -1
- package/.next/standalone/app/components/ScanStatusBar.js +97 -71
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/globals.css +15 -0
- package/.next/standalone/app/paths/page.js +0 -2
- package/.next/standalone/app/profile/page.js +0 -2
- package/.next/standalone/app/queue/page.js +0 -2
- package/.next/standalone/app/scores/page.js +0 -2
- package/.next/standalone/app/settings/page.js +0 -2
- package/.next/standalone/app/setup/page.js +0 -2
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +71 -62
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/island.js +36 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +126 -22
- package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{standalone/.next/static/chunks/1z_rt4jteewqp.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/CHANGELOG.md +57 -0
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/island.js +36 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/package.json +1 -1
- package/scripts/scrape.py +126 -22
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
- package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/standalone/.next/static/chunks/3z-swu5pet507.js +0 -1
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/static/chunks/3z-swu5pet507.js +0 -1
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
// (app/api/social/messages).
|
|
13
13
|
// chapters your jobs (Positions.csv) and the connections made in each
|
|
14
14
|
// posts your posts (Shares.csv) and the connections that came the week after
|
|
15
|
-
// invites requests to and from you (Invitations.csv), when the export has
|
|
15
|
+
// invites requests to and from you (Invitations.csv), when the export has
|
|
16
|
+
// them: counts, and one record per request (who, which way, when).
|
|
17
|
+
// The note sent with a request only with Keep my messages.
|
|
18
|
+
// names the names the export gives people, so someone who isn't a
|
|
19
|
+
// connection (and so isn't in the app's network) can be listed
|
|
20
|
+
//
|
|
21
|
+
// Every conversation is kept, whoever it's with and whatever folder it's in;
|
|
22
|
+
// Sponsored Messages and InMails are marked, not dropped (buildConversations).
|
|
16
23
|
//
|
|
17
24
|
// Plain functions, no React; tests/linkedin-export.test.mjs.
|
|
18
25
|
|
|
@@ -111,29 +118,86 @@ export function messageStats(rows = []) {
|
|
|
111
118
|
return { self, asOf: asOf || null, people };
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
/** A person with no profile link in the export, by their name: `name:ada quill`. */
|
|
122
|
+
export const nameOnlyKey = (name) => {
|
|
123
|
+
const n = nameKey(name);
|
|
124
|
+
return n ? `name:${n}` : null;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const SPONSORED = /sponsored/i;
|
|
128
|
+
const INMAIL = /in\s?mail/i;
|
|
129
|
+
// A company's or a showcase page's link as the sender: an advert, not a person.
|
|
130
|
+
const PAGE_SENDER = /linkedin\.com\/(company|showcase|school)\//i;
|
|
131
|
+
const yes = (v) => /^(yes|true|1)$/i.test(String(v || '').trim());
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What LinkedIn's rows say a conversation is: 'sponsored' (a Sponsored
|
|
135
|
+
* Message, an advert), 'inmail' (an InMail), or null for an ordinary one. The
|
|
136
|
+
* export marks neither in a column of its own, so it's read from the folder,
|
|
137
|
+
* the conversation's title, and a sender that's a company page, not a person.
|
|
138
|
+
*/
|
|
139
|
+
function kindOfRow(r) {
|
|
140
|
+
const folder = String(r.folder || '');
|
|
141
|
+
const title = String(r['conversation title'] || '');
|
|
142
|
+
if (SPONSORED.test(folder) || SPONSORED.test(title) || PAGE_SENDER.test(String(r['sender profile url'] || ''))) return 'sponsored';
|
|
143
|
+
if (INMAIL.test(folder) || INMAIL.test(title)) return 'inmail';
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
114
147
|
/**
|
|
115
|
-
* Every conversation in messages.csv
|
|
116
|
-
* { id, title, people, group, messages: [{ t, fromMe, text }] },
|
|
117
|
-
* first. `people` is everyone but you, by profile key
|
|
118
|
-
*
|
|
148
|
+
* Every conversation in messages.csv, one record each:
|
|
149
|
+
* { id, title, people, group, kind, folder, messages: [{ t, fromMe, text }] },
|
|
150
|
+
* messages oldest first. `people` is everyone but you, by profile key, or by
|
|
151
|
+
* name (nameOnlyKey) when the export has no link for them; more than one makes
|
|
152
|
+
* it a group, none (only you wrote, and to whom isn't said) leaves it empty.
|
|
153
|
+
* Nothing is left out for being in a folder: INBOX, SENT, ARCHIVE, SPAM and
|
|
154
|
+
* anything else LinkedIn adds all count; `folder` is the newest message's, in
|
|
155
|
+
* lower case. `kind` is 'sponsored', 'inmail' or null (kindOfRow). Drafts
|
|
156
|
+
* weren't sent, so they aren't messages; they're counted in `drafts`, and rows
|
|
157
|
+
* with no conversation or no readable date in `skipped`.
|
|
158
|
+
*
|
|
159
|
+
* You are found the way messageStats finds you, and by your name on rows with
|
|
160
|
+
* no link. `names` is everyone's name as the export spells it, by key, so a
|
|
161
|
+
* person who isn't a connection can still be shown. The words (the subject,
|
|
119
162
|
* when there is one, then the message) are read only with keepText: without it
|
|
120
163
|
* a message is a date and who sent it, and the title (a group's name) is left
|
|
121
164
|
* out too.
|
|
122
165
|
*/
|
|
123
166
|
export function buildConversations(rows = [], { keepText = false } = {}) {
|
|
124
167
|
const self = selfOf(rows);
|
|
168
|
+
// Your name as the export spells it, for rows that have no link for you.
|
|
169
|
+
const selfNames = new Map();
|
|
170
|
+
for (const r of rows) {
|
|
171
|
+
if (self && urlKey(r['sender profile url']) === self && r.from) selfNames.set(r.from, (selfNames.get(r.from) || 0) + 1);
|
|
172
|
+
}
|
|
173
|
+
const selfName = nameKey([...selfNames].sort((a, b) => b[1] - a[1])[0]?.[0] || '');
|
|
174
|
+
const isMe = (k) => !!k && (k === self || (!!selfName && k === `name:${selfName}`));
|
|
175
|
+
const names = {};
|
|
125
176
|
const convos = new Map();
|
|
177
|
+
let drafts = 0;
|
|
178
|
+
let skipped = 0;
|
|
126
179
|
for (const r of rows) {
|
|
127
|
-
const from = urlKey(r['sender profile url']);
|
|
128
180
|
const id = r['conversation id'];
|
|
129
|
-
if (!id || !from) continue;
|
|
130
181
|
const t = exportDate(r.date);
|
|
131
|
-
if (t == null) continue;
|
|
132
|
-
if (
|
|
182
|
+
if (!id || t == null) { skipped += 1; continue; }
|
|
183
|
+
if (yes(r['is message draft'])) { drafts += 1; continue; }
|
|
184
|
+
const fromUrl = urlKey(r['sender profile url']);
|
|
185
|
+
const from = fromUrl || nameOnlyKey(r.from);
|
|
186
|
+
if (from && r.from && !names[from]) names[from] = String(r.from).trim();
|
|
187
|
+
const toUrls = String(r['recipient profile urls'] || '').split(/[,;\s]+/).filter(Boolean).map(urlKey);
|
|
188
|
+
const toNames = String(r.to || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
189
|
+
// Names line up with links only when there are as many of each (a name can
|
|
190
|
+
// hold a comma: "Ben Ode, MBA"); without links, the names are all there is.
|
|
191
|
+
const to = toUrls.length ? toUrls : toNames.map(nameOnlyKey).filter(Boolean);
|
|
192
|
+
if (toUrls.length && toUrls.length === toNames.length) toUrls.forEach((u, i) => { if (!names[u]) names[u] = toNames[i]; });
|
|
193
|
+
if (!toUrls.length) toNames.forEach((n) => { const k = nameOnlyKey(n); if (k && !names[k]) names[k] = n; });
|
|
194
|
+
if (!convos.has(id)) convos.set(id, { id, title: '', everyone: new Set(), messages: [], kind: null, folder: '', newest: -Infinity });
|
|
133
195
|
const c = convos.get(id);
|
|
134
|
-
c.everyone.add(from);
|
|
135
|
-
|
|
136
|
-
|
|
196
|
+
if (from) c.everyone.add(from);
|
|
197
|
+
to.forEach((p) => c.everyone.add(p));
|
|
198
|
+
c.kind = c.kind || kindOfRow(r);
|
|
199
|
+
if (t >= c.newest) { c.newest = t; c.folder = String(r.folder || '').trim().toLowerCase().slice(0, 30); }
|
|
200
|
+
const m = { t, fromMe: isMe(from) };
|
|
137
201
|
if (keepText) {
|
|
138
202
|
m.text = [r.subject, r.content].map((x) => String(x || '').trim()).filter(Boolean).join('\n\n');
|
|
139
203
|
if (!c.title && r['conversation title']) c.title = String(r['conversation title']).trim();
|
|
@@ -142,37 +206,62 @@ export function buildConversations(rows = [], { keepText = false } = {}) {
|
|
|
142
206
|
}
|
|
143
207
|
const conversations = [];
|
|
144
208
|
for (const c of convos.values()) {
|
|
145
|
-
|
|
146
|
-
const people = [...c.everyone].filter((p) => p && p !== self).sort();
|
|
147
|
-
if (!people.length) continue;
|
|
209
|
+
const people = [...c.everyone].filter((p) => p && !isMe(p)).sort();
|
|
148
210
|
c.messages.sort((a, b) => a.t - b.t);
|
|
149
|
-
const out = { id: c.id, people, group: people.length > 1, messages: c.messages };
|
|
211
|
+
const out = { id: c.id, people, group: people.length > 1, kind: c.kind, folder: c.folder, messages: c.messages };
|
|
150
212
|
if (keepText) out.title = c.title;
|
|
151
213
|
conversations.push(out);
|
|
152
214
|
}
|
|
153
|
-
|
|
215
|
+
for (const k of Object.keys(names)) if (isMe(k)) delete names[k];
|
|
216
|
+
return { self, conversations, names, drafts, skipped };
|
|
154
217
|
}
|
|
155
218
|
|
|
156
|
-
/**
|
|
219
|
+
/**
|
|
220
|
+
* What the Conversations list keeps of one conversation, never any words:
|
|
221
|
+
* { id, people, group, kind, folder, last, lastFromThem, count, mine }. `mine`
|
|
222
|
+
* is how many of the messages you wrote; all of them means no one has answered.
|
|
223
|
+
*/
|
|
157
224
|
export function conversationSummary(c) {
|
|
158
225
|
const last = c.messages[c.messages.length - 1];
|
|
159
226
|
return {
|
|
160
227
|
id: c.id,
|
|
161
228
|
people: c.people,
|
|
162
229
|
group: c.group,
|
|
230
|
+
kind: c.kind || null,
|
|
231
|
+
folder: c.folder || '',
|
|
163
232
|
last: last ? last.t : null,
|
|
164
233
|
lastFromThem: last ? !last.fromMe : null,
|
|
165
234
|
count: c.messages.length,
|
|
235
|
+
mine: c.messages.filter((m) => m.fromMe).length,
|
|
166
236
|
};
|
|
167
237
|
}
|
|
168
238
|
|
|
239
|
+
/** The same people, by their names: a set's signature, or null when a name is missing. */
|
|
240
|
+
function nameSignature(keys, nameOf) {
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const k of keys) {
|
|
243
|
+
const n = nameKey(nameOf(k));
|
|
244
|
+
if (!n) return null;
|
|
245
|
+
out.push(n);
|
|
246
|
+
}
|
|
247
|
+
return out.sort().join('|');
|
|
248
|
+
}
|
|
249
|
+
|
|
169
250
|
/**
|
|
170
251
|
* The Conversations list: the export's conversations with what the live sync
|
|
171
252
|
* adds (unread, the thread's link, and a newer last message), plus a
|
|
172
|
-
* conversation for each person the sync found and the export doesn't
|
|
173
|
-
* (id `live:<key>`, count unknown).
|
|
253
|
+
* conversation for each person or group the sync found and the export doesn't
|
|
254
|
+
* have yet (id `live:<key>`, count unknown).
|
|
255
|
+
*
|
|
256
|
+
* live your connections, by their key (matchLive)
|
|
257
|
+
* others people who aren't your connections, by the link messaging gives
|
|
258
|
+
* them, with their name: matched to the export's conversation with
|
|
259
|
+
* someone of that name when only one has it
|
|
260
|
+
* groups group conversations: { threadUrl, people, names, last, unread },
|
|
261
|
+
* matched to the export's group with the same people by name
|
|
262
|
+
* names the export's names, by key
|
|
174
263
|
*/
|
|
175
|
-
export function conversationList(list = [], live = {}) {
|
|
264
|
+
export function conversationList(list = [], live = {}, { others = {}, groups = [], names = {} } = {}) {
|
|
176
265
|
const out = (Array.isArray(list) ? list : []).map((c) => ({ ...c, unread: null, threadUrl: null }));
|
|
177
266
|
const oneToOne = new Map();
|
|
178
267
|
for (const c of out) {
|
|
@@ -180,20 +269,59 @@ export function conversationList(list = [], live = {}) {
|
|
|
180
269
|
const had = oneToOne.get(c.people[0]);
|
|
181
270
|
if (!had || (c.last || 0) > (had.last || 0)) oneToOne.set(c.people[0], c);
|
|
182
271
|
}
|
|
272
|
+
const addLive = (c, v) => {
|
|
273
|
+
if (v.last && (!c.last || v.last > c.last)) {
|
|
274
|
+
c.last = v.last;
|
|
275
|
+
if (typeof v.lastFromThem === 'boolean') c.lastFromThem = v.lastFromThem;
|
|
276
|
+
}
|
|
277
|
+
c.unread = Number.isFinite(v.unread) ? v.unread : c.unread;
|
|
278
|
+
c.threadUrl = v.threadUrl || c.threadUrl;
|
|
279
|
+
};
|
|
280
|
+
const fresh = (id, people, group) => ({ id, people, group, kind: null, folder: '', last: null, lastFromThem: null, count: null, mine: null, unread: null, threadUrl: null });
|
|
183
281
|
for (const [key, v] of Object.entries(live || {})) {
|
|
184
282
|
if (!v || typeof v !== 'object') continue;
|
|
185
283
|
let c = oneToOne.get(key);
|
|
186
284
|
if (!c) {
|
|
187
|
-
c =
|
|
285
|
+
c = fresh(`live:${key}`, [key], false);
|
|
188
286
|
out.push(c);
|
|
189
287
|
oneToOne.set(key, c);
|
|
190
288
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
289
|
+
addLive(c, v);
|
|
290
|
+
}
|
|
291
|
+
// Someone who isn't a connection: the export knows them by their /in/name
|
|
292
|
+
// link, messaging by /in/ACoA…, so only their name can join the two.
|
|
293
|
+
const byName = new Map();
|
|
294
|
+
for (const [key, c] of oneToOne) {
|
|
295
|
+
const n = nameKey(names[key]);
|
|
296
|
+
if (n) byName.set(n, byName.has(n) ? null : c);
|
|
297
|
+
}
|
|
298
|
+
for (const [link, v] of Object.entries(others || {})) {
|
|
299
|
+
if (!v || typeof v !== 'object') continue;
|
|
300
|
+
let c = oneToOne.get(link) || byName.get(nameKey(v.name)) || null;
|
|
301
|
+
if (!c) {
|
|
302
|
+
c = fresh(`live:${link}`, [link], false);
|
|
303
|
+
out.push(c);
|
|
304
|
+
oneToOne.set(link, c);
|
|
194
305
|
}
|
|
195
|
-
c.
|
|
196
|
-
c
|
|
306
|
+
c.liveKey = link;
|
|
307
|
+
addLive(c, v);
|
|
308
|
+
}
|
|
309
|
+
const groupsByNames = new Map();
|
|
310
|
+
for (const c of out) {
|
|
311
|
+
if (!c.group) continue;
|
|
312
|
+
const sig = nameSignature(c.people, (k) => names[k]);
|
|
313
|
+
if (sig) groupsByNames.set(sig, groupsByNames.has(sig) ? null : c);
|
|
314
|
+
}
|
|
315
|
+
for (const g of Array.isArray(groups) ? groups : []) {
|
|
316
|
+
if (!g || !Array.isArray(g.people) || !g.people.length) continue;
|
|
317
|
+
const sig = nameSignature(g.people, (k) => g.names?.[k] || names[k]);
|
|
318
|
+
let c = (sig && groupsByNames.get(sig)) || null;
|
|
319
|
+
if (!c) {
|
|
320
|
+
c = fresh(`live:${g.threadUrl || g.people.join(',')}`, g.people, true);
|
|
321
|
+
out.push(c);
|
|
322
|
+
if (sig) groupsByNames.set(sig, c);
|
|
323
|
+
}
|
|
324
|
+
addLive(c, g);
|
|
197
325
|
}
|
|
198
326
|
return out;
|
|
199
327
|
}
|
|
@@ -268,7 +396,32 @@ export function invitationSplit(rows = []) {
|
|
|
268
396
|
return { incoming, outgoing, total: incoming + outgoing };
|
|
269
397
|
}
|
|
270
398
|
|
|
271
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Every request in Invitations.csv, one record each, newest first:
|
|
401
|
+
* { id, dir: 'in' | 'out', key, name, t, note? }. `key` is the other person's
|
|
402
|
+
* profile link (inviterProfileUrl or inviteeProfileUrl, in newer exports), or
|
|
403
|
+
* their name (nameOnlyKey) when there's no link. The note sent with a request
|
|
404
|
+
* is written by a person, like a message, so it's read only with keepText.
|
|
405
|
+
*/
|
|
406
|
+
export function buildInvitations(rows = [], { keepText = false } = {}) {
|
|
407
|
+
const out = [];
|
|
408
|
+
for (const r of rows) {
|
|
409
|
+
const d = String(r.direction || '').toUpperCase();
|
|
410
|
+
if (d !== 'INCOMING' && d !== 'OUTGOING') continue;
|
|
411
|
+
const dir = d === 'INCOMING' ? 'in' : 'out';
|
|
412
|
+
const name = String((dir === 'in' ? r.from : r.to) || '').trim();
|
|
413
|
+
const url = urlKey(dir === 'in' ? r.inviterprofileurl : r.inviteeprofileurl);
|
|
414
|
+
const key = (url && /\/in\//.test(url) ? url : null) || nameOnlyKey(name);
|
|
415
|
+
if (!key) continue;
|
|
416
|
+
const t = exportDate(r['sent at']);
|
|
417
|
+
const inv = { id: `${dir}|${key}|${t ?? ''}`, dir, key, name: name.slice(0, 120), t };
|
|
418
|
+
if (keepText && String(r.message || '').trim()) inv.note = String(r.message).trim().slice(0, 2000);
|
|
419
|
+
out.push(inv);
|
|
420
|
+
}
|
|
421
|
+
return out.sort((a, b) => (b.t || 0) - (a.t || 0));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const nameKey = (s) => String(s || '').split(',')[0].normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
272
425
|
.toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
273
426
|
|
|
274
427
|
const THREAD_LINK = /^https:\/\/www\.linkedin\.com\/messaging\/[\w\-/%=.]*$/;
|
|
@@ -288,16 +441,8 @@ function previewOf(p) {
|
|
|
288
441
|
return { t, fromMe: p.fromMe === true, text };
|
|
289
442
|
}
|
|
290
443
|
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
-
* messaging gives a member link (/in/ACoA…) your connections list doesn't have,
|
|
294
|
-
* so a person is matched by their link when it's the same, otherwise by their
|
|
295
|
-
* name when exactly one connection has it. Only the match is kept, never the
|
|
296
|
-
* name. Returns { live: { connectionKey: { last, unread, threadUrl,
|
|
297
|
-
* lastFromThem, preview } }, unmatched }; the caller decides whether the
|
|
298
|
-
* preview's words are kept.
|
|
299
|
-
*/
|
|
300
|
-
export function matchLive(incoming = {}, connections = []) {
|
|
444
|
+
/** (link, name) → your connection's key: by link when it's the same, else by a name only one connection has; null otherwise. */
|
|
445
|
+
function connectionMatcher(connections = []) {
|
|
301
446
|
const byUrl = new Map();
|
|
302
447
|
const byName = new Map();
|
|
303
448
|
for (const c of connections) {
|
|
@@ -306,21 +451,97 @@ export function matchLive(incoming = {}, connections = []) {
|
|
|
306
451
|
const n = nameKey(c.name);
|
|
307
452
|
if (n) byName.set(n, byName.has(n) ? null : key); // null: two people share it
|
|
308
453
|
}
|
|
454
|
+
return (url, name) => byUrl.get(keyFor({ profile_url: url })) || byName.get(nameKey(name)) || null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const PROFILE = /^https:\/\/www\.linkedin\.com\/in\/[^/?#\s]+\/?$/;
|
|
458
|
+
const cleanName = (s) => (typeof s === 'string' ? s.replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, 120) : '');
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The live messages sync's people, matched to your connections. LinkedIn's
|
|
462
|
+
* messaging gives a member link (/in/ACoA…) your connections list doesn't have,
|
|
463
|
+
* so a person is matched by their link when it's the same, otherwise by their
|
|
464
|
+
* name when exactly one connection has it. For a connection only the match is
|
|
465
|
+
* kept, never the name: the app has it already. Returns
|
|
466
|
+
*
|
|
467
|
+
* live { connectionKey: { last, unread, threadUrl, lastFromThem, preview } }
|
|
468
|
+
* others { link: { name, ...the same } }: people who aren't your
|
|
469
|
+
* connections. Their name is kept (and nothing else says who they
|
|
470
|
+
* are), so the Social tab can list them.
|
|
471
|
+
* unmatched how many are in `others`
|
|
472
|
+
*
|
|
473
|
+
* The caller decides whether the previews' words are kept.
|
|
474
|
+
*/
|
|
475
|
+
export function matchLive(incoming = {}, connections = []) {
|
|
476
|
+
const match = connectionMatcher(connections);
|
|
309
477
|
const live = {};
|
|
310
|
-
|
|
478
|
+
const others = {};
|
|
311
479
|
for (const [url, v] of Object.entries(incoming || {})) {
|
|
312
480
|
if (!v || typeof v !== 'object') continue;
|
|
313
|
-
const key =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (live[key] && (live[key].last || 0) >= (last || 0)) continue;
|
|
317
|
-
live[key] = {
|
|
318
|
-
last,
|
|
481
|
+
const key = match(url, v.name);
|
|
482
|
+
const entry = {
|
|
483
|
+
last: Number(v.last) || null,
|
|
319
484
|
unread: Number.isFinite(Number(v.unread)) && v.unread !== null ? Number(v.unread) : null,
|
|
320
485
|
threadUrl: threadLink(v.threadUrl),
|
|
321
486
|
lastFromThem: typeof v.lastFromThem === 'boolean' ? v.lastFromThem : null,
|
|
322
487
|
preview: previewOf(v.preview),
|
|
323
488
|
};
|
|
489
|
+
if (!key) {
|
|
490
|
+
const link = keyFor({ profile_url: url });
|
|
491
|
+
if (PROFILE.test(url) && !others[link]) others[link] = { name: cleanName(v.name), ...entry };
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (live[key] && (live[key].last || 0) >= (entry.last || 0)) continue;
|
|
495
|
+
live[key] = entry;
|
|
496
|
+
}
|
|
497
|
+
return { live, others, unmatched: Object.keys(others).length };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The live sync's group conversations, each member matched to your connections
|
|
502
|
+
* as matchLive does: { threadUrl, people, names, last, unread }. `people` are
|
|
503
|
+
* connection keys where matched, else the link messaging gave; `names` keeps
|
|
504
|
+
* the names of the ones who aren't connections only. No words: a group's
|
|
505
|
+
* newest message isn't read.
|
|
506
|
+
*/
|
|
507
|
+
export function matchLiveGroups(groups = [], connections = []) {
|
|
508
|
+
const match = connectionMatcher(connections);
|
|
509
|
+
const out = [];
|
|
510
|
+
for (const g of Array.isArray(groups) ? groups.slice(0, 5000) : []) {
|
|
511
|
+
if (!g || typeof g !== 'object' || !Array.isArray(g.people)) continue;
|
|
512
|
+
const people = [];
|
|
513
|
+
const names = {};
|
|
514
|
+
for (const url of g.people.slice(0, 200)) {
|
|
515
|
+
if (typeof url !== 'string' || !PROFILE.test(url)) continue;
|
|
516
|
+
const name = cleanName(g.names?.[url]);
|
|
517
|
+
const key = match(url, name);
|
|
518
|
+
if (key) { people.push(key); continue; }
|
|
519
|
+
const link = keyFor({ profile_url: url });
|
|
520
|
+
people.push(link);
|
|
521
|
+
if (name) names[link] = name;
|
|
522
|
+
}
|
|
523
|
+
if (people.length < 2) continue;
|
|
524
|
+
out.push({
|
|
525
|
+
threadUrl: threadLink(g.threadUrl),
|
|
526
|
+
people: [...new Set(people)].sort(),
|
|
527
|
+
names,
|
|
528
|
+
last: Number(g.last) || null,
|
|
529
|
+
unread: Number.isFinite(Number(g.unread)) && g.unread !== null ? Number(g.unread) : null,
|
|
530
|
+
lastFromThem: typeof g.lastFromThem === 'boolean' ? g.lastFromThem : null,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A new live sync over what earlier ones found: per person (or group), the
|
|
538
|
+
* newer of the two. A quick sync reads only the top of the list, so what a
|
|
539
|
+
* full-history sync found further down stays until something newer replaces it.
|
|
540
|
+
*/
|
|
541
|
+
export function mergeLive(before = {}, after = {}) {
|
|
542
|
+
const out = { ...(before && typeof before === 'object' ? before : {}) };
|
|
543
|
+
for (const [k, v] of Object.entries(after || {})) {
|
|
544
|
+
if (!out[k] || (v.last || 0) >= (out[k].last || 0)) out[k] = v;
|
|
324
545
|
}
|
|
325
|
-
return
|
|
546
|
+
return out;
|
|
326
547
|
}
|