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.
Files changed (242) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +4 -4
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
  42. package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
  43. package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
  44. package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
  45. package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
  46. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
  47. package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
  48. package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
  49. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/social/route.js +4 -3
  51. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/import.html +1 -1
  60. package/.next/standalone/.next/server/app/import.rsc +15 -15
  61. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  62. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  63. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  64. package/.next/standalone/.next/server/app/index.html +1 -1
  65. package/.next/standalone/.next/server/app/index.rsc +15 -15
  66. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  67. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  68. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/launch.html +2 -2
  72. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  73. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  74. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  75. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  76. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/paths.html +1 -1
  81. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  82. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  83. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  84. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  85. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/app/profile.html +1 -1
  88. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  89. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  90. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  91. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  92. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/app/queue.html +1 -1
  95. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  96. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  97. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  98. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  99. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/scores.html +1 -1
  102. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  103. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  105. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  106. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/settings.html +1 -1
  109. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  113. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/setup.html +1 -1
  116. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  117. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  119. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  120. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/social.html +1 -1
  123. package/.next/standalone/.next/server/app/social.rsc +15 -15
  124. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  127. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  128. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__05_3-22._.js} +7 -2
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
  131. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/_1_fynia._.js +4 -4
  133. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
  137. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +7 -7
  138. package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
  139. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
  144. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  145. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  146. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
  147. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  148. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  149. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  150. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +2 -1
  151. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
  152. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  153. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  154. package/.next/standalone/.next/server/pages/404.html +1 -1
  155. package/.next/standalone/.next/server/pages/500.html +1 -1
  156. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  157. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  158. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
  159. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +1 -0
  160. package/.next/standalone/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
  161. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +1 -0
  162. package/.next/standalone/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
  163. package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
  164. package/.next/standalone/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
  165. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +1 -0
  166. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
  167. package/.next/standalone/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
  168. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
  169. package/.next/{static/chunks/1z_rt4jteewqp.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
  170. package/.next/standalone/CHANGELOG.md +57 -0
  171. package/.next/standalone/app/api/data/export/route.js +2 -1
  172. package/.next/standalone/app/api/scraper/route.js +5 -1
  173. package/.next/standalone/app/api/social/crm/route.js +37 -0
  174. package/.next/standalone/app/api/social/messages/route.js +44 -8
  175. package/.next/standalone/app/api/social/route.js +80 -22
  176. package/.next/standalone/app/components/GalaxyLab.js +4 -1
  177. package/.next/standalone/app/components/ScanStatusBar.js +97 -71
  178. package/.next/standalone/app/components/settings/DataSection.js +12 -3
  179. package/.next/standalone/app/globals.css +15 -0
  180. package/.next/standalone/app/paths/page.js +0 -2
  181. package/.next/standalone/app/profile/page.js +0 -2
  182. package/.next/standalone/app/queue/page.js +0 -2
  183. package/.next/standalone/app/scores/page.js +0 -2
  184. package/.next/standalone/app/settings/page.js +0 -2
  185. package/.next/standalone/app/setup/page.js +0 -2
  186. package/.next/standalone/app/social/Crm.js +683 -0
  187. package/.next/standalone/app/social/Thread.js +90 -0
  188. package/.next/standalone/app/social/page.js +71 -62
  189. package/.next/standalone/lib/data-export.js +10 -4
  190. package/.next/standalone/lib/data-folder.js +9 -0
  191. package/.next/standalone/lib/data-import.js +13 -3
  192. package/.next/standalone/lib/island.js +36 -0
  193. package/.next/standalone/lib/linkedin-export.js +267 -46
  194. package/.next/standalone/lib/social-crm.js +415 -0
  195. package/.next/standalone/lib/social-store.js +13 -6
  196. package/.next/standalone/package-lock.json +2 -2
  197. package/.next/standalone/package.json +1 -1
  198. package/.next/standalone/scripts/scrape.py +126 -22
  199. package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
  200. package/.next/static/chunks/15sjx07xhpn6p.js +1 -0
  201. package/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
  202. package/.next/static/chunks/1a74dky22dxd6.js +1 -0
  203. package/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
  204. package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
  205. package/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
  206. package/.next/static/chunks/297e1z34yi--z.js +1 -0
  207. package/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
  208. package/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
  209. package/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
  210. package/.next/{standalone/.next/static/chunks/1z_rt4jteewqp.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
  211. package/CHANGELOG.md +57 -0
  212. package/lib/data-export.js +10 -4
  213. package/lib/data-folder.js +9 -0
  214. package/lib/data-import.js +13 -3
  215. package/lib/island.js +36 -0
  216. package/lib/linkedin-export.js +267 -46
  217. package/lib/social-crm.js +415 -0
  218. package/lib/social-store.js +13 -6
  219. package/package.json +1 -1
  220. package/scripts/scrape.py +126 -22
  221. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
  222. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
  223. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
  224. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
  225. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
  226. package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +0 -1
  227. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  228. package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +0 -1
  229. package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
  230. package/.next/standalone/.next/static/chunks/3z-swu5pet507.js +0 -1
  231. package/.next/standalone/app/social/Conversations.js +0 -246
  232. package/.next/static/chunks/2bftn9gubwf0w.js +0 -1
  233. package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  234. package/.next/static/chunks/39csdcx7yv227.js +0 -1
  235. package/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
  236. package/.next/static/chunks/3z-swu5pet507.js +0 -1
  237. /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
  238. /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
  239. /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
  240. /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
  241. /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
  242. /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 them
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 you're part of, one record each:
116
- * { id, title, people, group, messages: [{ t, fromMe, text }] }, messages oldest
117
- * first. `people` is everyone but you, by profile key; more than one makes it a
118
- * group. You are found the way messageStats finds you. The words (the subject,
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 (!convos.has(id)) convos.set(id, { id, title: '', everyone: new Set(), messages: [] });
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
- String(r['recipient profile urls'] || '').split(/[,;\s]+/).filter(Boolean).forEach((u) => c.everyone.add(urlKey(u)));
136
- const m = { t, fromMe: from === self };
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
- if (!c.everyone.has(self)) continue;
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
- return { self, conversations };
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
- /** What the Conversations list keeps of one conversation, never any words: { id, people, group, last, lastFromThem, count }. */
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 have yet
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 = { id: `live:${key}`, people: [key], group: false, last: null, lastFromThem: null, count: null, unread: null, threadUrl: null };
285
+ c = fresh(`live:${key}`, [key], false);
188
286
  out.push(c);
189
287
  oneToOne.set(key, c);
190
288
  }
191
- if (v.last && (!c.last || v.last > c.last)) {
192
- c.last = v.last;
193
- if (typeof v.lastFromThem === 'boolean') c.lastFromThem = v.lastFromThem;
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.unread = Number.isFinite(v.unread) ? v.unread : c.unread;
196
- c.threadUrl = v.threadUrl || c.threadUrl;
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
- const nameKey = (s) => String(s || '').split(',')[0].normalize('NFD').replace(/[̀-ͯ]/g, '')
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
- * The live messages sync's people, matched to your connections. LinkedIn's
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
- let unmatched = 0;
478
+ const others = {};
311
479
  for (const [url, v] of Object.entries(incoming || {})) {
312
480
  if (!v || typeof v !== 'object') continue;
313
- const key = byUrl.get(keyFor({ profile_url: url })) || byName.get(nameKey(v.name)) || null;
314
- if (!key) { unmatched += 1; continue; }
315
- const last = Number(v.last) || null;
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 { live, unmatched };
546
+ return out;
326
547
  }