six-degrees 0.4.8 → 0.4.9

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 (247) 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 +3 -3
  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 +12 -12
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  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 +2 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/route.js +2 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/scraper/route.js +2 -2
  39. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +2 -1
  43. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +2 -1
  45. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
  48. package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
  49. package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
  50. package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
  51. package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
  52. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
  53. package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
  54. package/.next/standalone/.next/server/app/api/social/route.js +4 -2
  55. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +14 -14
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +14 -14
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/social.html +1 -1
  128. package/.next/standalone/.next/server/app/social.rsc +14 -14
  129. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
  140. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
  143. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
  144. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  145. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
  146. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
  148. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +1 -1
  149. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  150. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +1 -1
  151. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
  152. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
  155. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
  156. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
  158. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
  159. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
  160. package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
  165. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
  167. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  169. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  172. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  174. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
  176. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
  177. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  178. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  179. package/.next/standalone/.next/server/pages/404.html +1 -1
  180. package/.next/standalone/.next/server/pages/500.html +1 -1
  181. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  182. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  183. package/.next/{static/chunks/1z-zp93cyoxvz.js → standalone/.next/static/chunks/04bbi55urkrd6.js} +1 -1
  184. package/.next/{static/chunks/3q3pmt5vv-4pi.js → standalone/.next/static/chunks/054zdthcnyd2h.js} +1 -1
  185. package/.next/standalone/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
  186. package/.next/{static/chunks/3zw8bv7-mc4n6.js → standalone/.next/static/chunks/1z_rt4jteewqp.js} +1 -1
  187. package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  188. package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
  189. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  190. package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +1 -0
  191. package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  192. package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  193. package/.next/standalone/CHANGELOG.md +39 -0
  194. package/.next/standalone/app/api/social/messages/route.js +130 -0
  195. package/.next/standalone/app/api/social/route.js +89 -47
  196. package/.next/standalone/app/components/ScanStatusBar.js +40 -3
  197. package/.next/standalone/app/components/settings/DataSection.js +2 -1
  198. package/.next/standalone/app/paths/page.js +2 -0
  199. package/.next/standalone/app/profile/page.js +2 -0
  200. package/.next/standalone/app/queue/page.js +2 -0
  201. package/.next/standalone/app/scores/page.js +2 -0
  202. package/.next/standalone/app/settings/page.js +2 -0
  203. package/.next/standalone/app/setup/page.js +6 -3
  204. package/.next/standalone/app/social/Conversations.js +246 -0
  205. package/.next/standalone/app/social/page.js +90 -9
  206. package/.next/standalone/lib/galaxy-lab.js +6 -1
  207. package/.next/standalone/lib/linkedin-export.js +175 -5
  208. package/.next/standalone/lib/social-store.js +73 -0
  209. package/.next/standalone/package-lock.json +2 -2
  210. package/.next/standalone/package.json +1 -1
  211. package/.next/standalone/scripts/scrape.py +156 -23
  212. package/.next/{standalone/.next/static/chunks/1z-zp93cyoxvz.js → static/chunks/04bbi55urkrd6.js} +1 -1
  213. package/.next/{standalone/.next/static/chunks/3q3pmt5vv-4pi.js → static/chunks/054zdthcnyd2h.js} +1 -1
  214. package/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
  215. package/.next/{standalone/.next/static/chunks/3zw8bv7-mc4n6.js → static/chunks/1z_rt4jteewqp.js} +1 -1
  216. package/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  217. package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
  218. package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  219. package/.next/static/chunks/39csdcx7yv227.js +1 -0
  220. package/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  221. package/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  222. package/CHANGELOG.md +39 -0
  223. package/lib/galaxy-lab.js +6 -1
  224. package/lib/linkedin-export.js +175 -5
  225. package/lib/social-store.js +73 -0
  226. package/package.json +1 -1
  227. package/scripts/scrape.py +156 -23
  228. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
  229. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
  230. package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
  231. package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
  232. package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
  233. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
  234. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  235. package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  236. package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
  237. package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
  238. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  239. package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  240. package/.next/static/chunks/14m1zu-8warte.js +0 -1
  241. package/.next/static/chunks/15qbli9udo0vh.js +0 -1
  242. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  243. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  244. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
  245. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  246. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  247. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
@@ -6,6 +6,10 @@
6
6
  //
7
7
  // people per profile: when you last messaged, who wrote last, how many
8
8
  // messages (1:1 conversations only). Never a message's text.
9
+ // conversations per conversation: who's in it, when it was last active, who
10
+ // wrote last, how many messages. The text itself only when you turn
11
+ // on Keep my messages, and then in a file of its own
12
+ // (app/api/social/messages).
9
13
  // chapters your jobs (Positions.csv) and the connections made in each
10
14
  // posts your posts (Shares.csv) and the connections that came the week after
11
15
  // invites requests to and from you (Invitations.csv), when the export has them
@@ -49,14 +53,29 @@ export function exportDate(s) {
49
53
 
50
54
  const urlKey = (u) => keyFor({ profile_url: u });
51
55
 
56
+ /** You, in messages.csv: the sender in the most conversations. */
57
+ function selfOf(rows = []) {
58
+ const senders = new Map();
59
+ for (const r of rows) {
60
+ const from = urlKey(r['sender profile url']);
61
+ const id = r['conversation id'];
62
+ if (!id || !from) continue;
63
+ if (!senders.has(from)) senders.set(from, new Set());
64
+ senders.get(from).add(id);
65
+ }
66
+ let self = null;
67
+ for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
68
+ return self;
69
+ }
70
+
52
71
  /**
53
72
  * From messages.csv, per person (by profile): { last, lastFromThem, total, recent }.
54
73
  * `self` is you: the sender in the most conversations. Only 1:1 conversations
55
74
  * count; group threads say little about one relationship. No text is read.
56
75
  */
57
76
  export function messageStats(rows = []) {
77
+ const self = selfOf(rows);
58
78
  const convos = new Map();
59
- const senders = new Map();
60
79
  for (const r of rows) {
61
80
  const from = urlKey(r['sender profile url']);
62
81
  const id = r['conversation id'];
@@ -69,11 +88,7 @@ export function messageStats(rows = []) {
69
88
  c.people.add(from);
70
89
  to.forEach((p) => c.people.add(p));
71
90
  c.msgs.push({ from, t });
72
- if (!senders.has(from)) senders.set(from, new Set());
73
- senders.get(from).add(id);
74
91
  }
75
- let self = null;
76
- for (const [who, ids] of senders) if (!self || ids.size > senders.get(self).size) self = who;
77
92
  let asOf = 0;
78
93
  const people = new Map();
79
94
  for (const c of convos.values()) {
@@ -96,6 +111,104 @@ export function messageStats(rows = []) {
96
111
  return { self, asOf: asOf || null, people };
97
112
  }
98
113
 
114
+ /**
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,
119
+ * when there is one, then the message) are read only with keepText: without it
120
+ * a message is a date and who sent it, and the title (a group's name) is left
121
+ * out too.
122
+ */
123
+ export function buildConversations(rows = [], { keepText = false } = {}) {
124
+ const self = selfOf(rows);
125
+ const convos = new Map();
126
+ for (const r of rows) {
127
+ const from = urlKey(r['sender profile url']);
128
+ const id = r['conversation id'];
129
+ if (!id || !from) continue;
130
+ const t = exportDate(r.date);
131
+ if (t == null) continue;
132
+ if (!convos.has(id)) convos.set(id, { id, title: '', everyone: new Set(), messages: [] });
133
+ 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 };
137
+ if (keepText) {
138
+ m.text = [r.subject, r.content].map((x) => String(x || '').trim()).filter(Boolean).join('\n\n');
139
+ if (!c.title && r['conversation title']) c.title = String(r['conversation title']).trim();
140
+ }
141
+ c.messages.push(m);
142
+ }
143
+ const conversations = [];
144
+ 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;
148
+ c.messages.sort((a, b) => a.t - b.t);
149
+ const out = { id: c.id, people, group: people.length > 1, messages: c.messages };
150
+ if (keepText) out.title = c.title;
151
+ conversations.push(out);
152
+ }
153
+ return { self, conversations };
154
+ }
155
+
156
+ /** What the Conversations list keeps of one conversation, never any words: { id, people, group, last, lastFromThem, count }. */
157
+ export function conversationSummary(c) {
158
+ const last = c.messages[c.messages.length - 1];
159
+ return {
160
+ id: c.id,
161
+ people: c.people,
162
+ group: c.group,
163
+ last: last ? last.t : null,
164
+ lastFromThem: last ? !last.fromMe : null,
165
+ count: c.messages.length,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * The Conversations list: the export's conversations with what the live sync
171
+ * 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).
174
+ */
175
+ export function conversationList(list = [], live = {}) {
176
+ const out = (Array.isArray(list) ? list : []).map((c) => ({ ...c, unread: null, threadUrl: null }));
177
+ const oneToOne = new Map();
178
+ for (const c of out) {
179
+ if (c.group || c.people?.length !== 1) continue;
180
+ const had = oneToOne.get(c.people[0]);
181
+ if (!had || (c.last || 0) > (had.last || 0)) oneToOne.set(c.people[0], c);
182
+ }
183
+ for (const [key, v] of Object.entries(live || {})) {
184
+ if (!v || typeof v !== 'object') continue;
185
+ let c = oneToOne.get(key);
186
+ if (!c) {
187
+ c = { id: `live:${key}`, people: [key], group: false, last: null, lastFromThem: null, count: null, unread: null, threadUrl: null };
188
+ out.push(c);
189
+ oneToOne.set(key, c);
190
+ }
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;
194
+ }
195
+ c.unread = Number.isFinite(v.unread) ? v.unread : c.unread;
196
+ c.threadUrl = v.threadUrl || c.threadUrl;
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /**
202
+ * The order the Conversations list shows: waiting on you (they wrote last, one
203
+ * to one) first, then your S and A tier people, then the most recent.
204
+ * `tierOf(conversation)` gives its best tier.
205
+ */
206
+ export function sortConversations(list = [], tierOf = () => null) {
207
+ const waiting = (c) => (!c.group && c.lastFromThem === true ? 0 : 1);
208
+ const tier = (c) => ({ S: 0, A: 1 }[tierOf(c)] ?? 2);
209
+ return [...list].sort((a, b) => waiting(a) - waiting(b) || tier(a) - tier(b) || (b.last || 0) - (a.last || 0));
210
+ }
211
+
99
212
  /** Warm (in the last 30 days, or 3+ in 90), cool (within a year), dormant (longer ago), or never. */
100
213
  export function warmthOf(stat, asOf) {
101
214
  if (!stat || !stat.total) return 'never';
@@ -154,3 +267,60 @@ export function invitationSplit(rows = []) {
154
267
  }
155
268
  return { incoming, outgoing, total: incoming + outgoing };
156
269
  }
270
+
271
+ const nameKey = (s) => String(s || '').split(',')[0].normalize('NFD').replace(/[̀-ͯ]/g, '')
272
+ .toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
273
+
274
+ const THREAD_LINK = /^https:\/\/www\.linkedin\.com\/messaging\/[\w\-/%=.]*$/;
275
+
276
+ /** A link to a LinkedIn messaging thread, or null: nothing else is ever put behind "Open on LinkedIn". */
277
+ export function threadLink(u) {
278
+ const v = String(u || '').trim();
279
+ return THREAD_LINK.test(v) ? v : null;
280
+ }
281
+
282
+ /** A message the live sync read, as it's kept: { t, fromMe, text }, or null. */
283
+ function previewOf(p) {
284
+ if (!p || typeof p !== 'object') return null;
285
+ const t = Number(p.t) || null;
286
+ const text = typeof p.text === 'string' ? p.text.slice(0, 20000) : null;
287
+ if (!t && !text) return null;
288
+ return { t, fromMe: p.fromMe === true, text };
289
+ }
290
+
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 = []) {
301
+ const byUrl = new Map();
302
+ const byName = new Map();
303
+ for (const c of connections) {
304
+ const key = keyFor(c);
305
+ byUrl.set(key, key);
306
+ const n = nameKey(c.name);
307
+ if (n) byName.set(n, byName.has(n) ? null : key); // null: two people share it
308
+ }
309
+ const live = {};
310
+ let unmatched = 0;
311
+ for (const [url, v] of Object.entries(incoming || {})) {
312
+ 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,
319
+ unread: Number.isFinite(Number(v.unread)) && v.unread !== null ? Number(v.unread) : null,
320
+ threadUrl: threadLink(v.threadUrl),
321
+ lastFromThem: typeof v.lastFromThem === 'boolean' ? v.lastFromThem : null,
322
+ preview: previewOf(v.preview),
323
+ };
324
+ }
325
+ return { live, unmatched };
326
+ }
@@ -0,0 +1,73 @@
1
+ // Where the Social tab keeps what it knows, per profile, in the data folder:
2
+ //
3
+ // social-<profile>.json numbers and dates (app/api/social): who you
4
+ // messaged, when, who wrote last, the list of
5
+ // conversations. Never any words.
6
+ // social-messages-<profile>.json the messages themselves, only while Keep my
7
+ // messages is on (app/api/social/messages).
8
+ //
9
+ // Two files so that turning Keep my messages off, or Forget it, deletes the
10
+ // words by deleting one file, and so the everyday file the Galaxy and the
11
+ // Social tab read stays small however many messages there are. Neither is in
12
+ // "Save a copy of my network": that carries an allow-list of files
13
+ // (lib/data-folder.js TRAVELLING_FILES) and these aren't on it.
14
+
15
+ import { readFileSync, writeFileSync, renameSync, rmSync, statSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { dataDir } from './db-client';
18
+ import { resolveProfile } from './profile';
19
+
20
+ // The last messages file read, while it's unchanged (readMessages).
21
+ const cache = { file: null, mtimeMs: 0, size: 0, data: null };
22
+
23
+ /** The Social tab's files for the profile in use, or null before there is one. */
24
+ export function socialFiles() {
25
+ const me = resolveProfile({ create: false });
26
+ if (!me?.id || !/^[\w-]+$/.test(String(me.id))) return null;
27
+ const dir = dataDir();
28
+ const messages = path.join(dir, `social-messages-${me.id}.json`);
29
+ return {
30
+ profileId: me.id,
31
+ social: path.join(dir, `social-${me.id}.json`),
32
+ messages,
33
+ // An import's messages arrive in parts (each request stays under the 10 MB
34
+ // middleware reads); they gather here and replace `messages` at the end.
35
+ incoming: `${messages}.incoming`,
36
+ };
37
+ }
38
+
39
+ export function readJson(file, fallback = null) {
40
+ try { return JSON.parse(readFileSync(file, 'utf8')) ?? fallback; } catch { return fallback; }
41
+ }
42
+
43
+ /** Written whole or not at all: a crash mid-write never leaves half a file. */
44
+ export function writeJsonAtomic(file, value) {
45
+ const tmp = `${file}.${process.pid}.tmp`;
46
+ writeFileSync(tmp, JSON.stringify(value));
47
+ renameSync(tmp, file);
48
+ if (cache.file === file) cache.file = null;
49
+ }
50
+
51
+ /** The kept messages and their live-sync previews, or empty ones. */
52
+ export function readMessages(file) {
53
+ // Kept in memory while the file is unchanged: a long history is read once,
54
+ // not on every page of a thread or every letter typed in the search box.
55
+ let st;
56
+ try { st = statSync(file); } catch { return { threads: {}, previews: {} }; }
57
+ if (cache.file === file && cache.mtimeMs === st.mtimeMs && cache.size === st.size) return cache.data;
58
+ const raw = readJson(file, {});
59
+ const data = {
60
+ threads: raw?.threads && typeof raw.threads === 'object' ? raw.threads : {},
61
+ previews: raw?.previews && typeof raw.previews === 'object' ? raw.previews : {},
62
+ };
63
+ Object.assign(cache, { file, mtimeMs: st.mtimeMs, size: st.size, data });
64
+ return data;
65
+ }
66
+
67
+ /** The messages gone: the kept file and any import still arriving. */
68
+ export function deleteMessages(files) {
69
+ if (!files) return;
70
+ rmSync(files.messages, { force: true });
71
+ rmSync(files.incoming, { force: true });
72
+ cache.file = null;
73
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "six-degrees",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "six-degrees",
9
- "version": "0.4.8",
9
+ "version": "0.4.9",
10
10
  "license": "MIT",
11
11
  "os": [
12
12
  "darwin",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "six-degrees",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
5
5
  "license": "MIT",
6
6
  "author": "Blake Burford",
@@ -59,7 +59,7 @@ _ENV_LOCAL = _load_env_local()
59
59
  # a server they don't control.
60
60
  APP_URL = os.getenv("APP_URL", "http://localhost:3000")
61
61
  MESSAGING_URL = "https://www.linkedin.com/messaging/"
62
- MESSAGE_SCROLLS = 6 # scrolls down the conversation list, a few seconds apart
62
+ MESSAGE_SCROLLS = 15
63
63
 
64
64
  CONNECTIONS_URL = "https://www.linkedin.com/mynetwork/invite-connect/connections/"
65
65
 
@@ -330,7 +330,7 @@ def ensure_logged_in(page, timeout_s=LOGIN_WAIT_SECONDS, log_fn=None, stop_on_ch
330
330
  if stop_on_checkpoint and signed_out:
331
331
  reason = "a security check" if wall == "checkpoint" else "being signed out"
332
332
  _keep_pushback_evidence(page, reason)
333
- set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
333
+ set_cooldown(seconds=AUTO_PUSHBACK_REST if EXPERIMENT["on"] else DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
334
334
  raise LinkedInPushedBack(0, reason)
335
335
 
336
336
  if _has_session_cookie(context) and not _looks_logged_out(page) and not wall:
@@ -1282,10 +1282,17 @@ LEGACY_PAGES_READ = 10 # how far every read before 0.1.6 went, at mos
1282
1282
  # A sitting of SESSION_PAGES searches, then a long rest; searches only in the
1283
1283
  # daytime on this computer's clock; and at the daily budget it waits for the
1284
1284
  # budget to free up instead of stopping. Pages are saved one at a time.
1285
+ #
1286
+ # Its own ceilings sit under whatever budget is set on the Scan page: the account
1287
+ # restricted on 2026-09-28 did 373 searches in 24 hours with the budget raised to
1288
+ # 500. Auto scan stays far below that however high the budget is set.
1285
1289
  EXPERIMENT = {"on": False, "pages": 0, "wire": 0}
1286
- SESSION_PAGES = 10 # searches in one sitting
1287
- SESSION_REST = 45 * 60 # the rest after each sitting
1288
- DRIP_HOURS = (9, 19) # searches only from 09:00 to 19:00, local time
1290
+ SESSION_PAGES = 8 # searches in one sitting
1291
+ SESSION_REST = 60 * 60 # the rest after each sitting
1292
+ DRIP_HOURS = (9, 18) # searches only from 09:00 to 18:00, local time
1293
+ AUTO_DAY_CAP = 40 # at most this many searches in any 24 hours
1294
+ AUTO_WEEK_CAP = 200 # and this many in any 7 days
1295
+ AUTO_PUSHBACK_REST = 2 * 24 * 3600 # after any LinkedIn check, nothing for two days
1289
1296
  WIRE_SAMPLES = 5 # raw LinkedIn responses kept per run, for research
1290
1297
 
1291
1298
 
@@ -1899,6 +1906,18 @@ def voyager_total(payload):
1899
1906
  return None
1900
1907
 
1901
1908
 
1909
+ _WORDS = {"body", "text", "subject", "attributedBody", "snippet", "messages", "headline", "firstName", "lastName", "rootUrl", "artifacts", "sponsoredPreviewText", "customPronoun", "advertiserLabel"}
1910
+
1911
+
1912
+ def _redact_words(x):
1913
+ """A copy of a LinkedIn response with every piece of writing and every name removed."""
1914
+ if isinstance(x, dict):
1915
+ return {k: ("<removed>" if k in _WORDS else _redact_words(v)) for k, v in x.items()}
1916
+ if isinstance(x, list):
1917
+ return [_redact_words(v) for v in x]
1918
+ return x
1919
+
1920
+
1902
1921
  def _wire_sample(data, prefix="search"):
1903
1922
  if EXPERIMENT["wire"] >= WIRE_SAMPLES:
1904
1923
  return
@@ -1908,6 +1927,10 @@ def _wire_sample(data, prefix="search"):
1908
1927
  return
1909
1928
  folder = _home() / "wire-samples"
1910
1929
  folder.mkdir(parents=True, exist_ok=True)
1930
+ if prefix == "messages":
1931
+ # Messaging responses carry what people wrote, and their names: a sample
1932
+ # keeps only the shape (keys, links, dates, counts), never the words.
1933
+ raw = json.dumps(_redact_words(data))
1911
1934
  (folder / f"{prefix}-{int(time.time() * 1000)}.json").write_text(raw)
1912
1935
  EXPERIMENT["wire"] += 1
1913
1936
  except Exception:
@@ -1948,14 +1971,51 @@ def _wire_merge(page, profile_url, page_results):
1948
1971
  return page_results
1949
1972
 
1950
1973
 
1951
- _PROFILE_LINK = re.compile(r"https://www\\.linkedin\\.com/in/[^/?#\\s\"']+")
1974
+ _PROFILE_LINK = re.compile(r"https://www\.linkedin\.com/in/[^/?#\s\"']+")
1975
+
1976
+
1977
+ def _dig(x, *keys):
1978
+ """x[k1][k2]..., or None as soon as a step isn't there."""
1979
+ for k in keys:
1980
+ if not isinstance(x, dict):
1981
+ return None
1982
+ x = x.get(k)
1983
+ return x
1984
+
1985
+
1986
+ def _link_key(link):
1987
+ """A profile link as the messages sync compares them: no query, one closing slash."""
1988
+ return link.split("?")[0].rstrip("/") + "/" if isinstance(link, str) and "/in/" in link else None
1952
1989
 
1953
1990
 
1954
1991
  def voyager_conversations(payload):
1955
1992
  """Conversations in a LinkedIn messaging response (experimental): per conversation,
1956
- everyone in it (profile links), when it was last active, and unread messages.
1957
- Anything with a lastActivityAt and participants, wherever it sits. Never the text."""
1993
+ everyone in it (profile links), when it was last active, unread messages, its link,
1994
+ whether it's a group, and the newest message LinkedIn sent along with it (when,
1995
+ from whom, and its words). Anything with a lastActivityAt and participants,
1996
+ wherever it sits. The words are handed to the app only while Keep my messages is
1997
+ on there (sync_messages); a wire sample never has them (_redact_words)."""
1958
1998
  out = []
1999
+ names = {}
2000
+
2001
+ def text_of(v):
2002
+ return v.get("text", "") if isinstance(v, dict) else (v if isinstance(v, str) else "")
2003
+
2004
+ def latest_of(x):
2005
+ # conversation.messages.elements[]: body.text, deliveredAt, and who sent it
2006
+ # (actor.participantType.member.profileUrl, the same /in/ACoA... link the
2007
+ # participants have, so it tells you from them).
2008
+ msgs = x.get("messages")
2009
+ elements = msgs.get("elements") if isinstance(msgs, dict) else msgs
2010
+ best = None
2011
+ for m in elements if isinstance(elements, list) else []:
2012
+ t = _dig(m, "deliveredAt")
2013
+ if not isinstance(t, (int, float)) or t < 1e12 or (best and t <= best["t"]):
2014
+ continue
2015
+ text = text_of(_dig(m, "body")).strip()
2016
+ best = {"t": int(t), "from": _link_key(_dig(m, "actor", "participantType", "member", "profileUrl")),
2017
+ "text": text or None}
2018
+ return best
1959
2019
 
1960
2020
  def people(x):
1961
2021
  found = set()
@@ -1966,6 +2026,14 @@ def voyager_conversations(payload):
1966
2026
  pid = v.get("publicIdentifier")
1967
2027
  if isinstance(pid, str) and pid and pid != "UNKNOWN":
1968
2028
  found.add(f"https://www.linkedin.com/in/{pid}/")
2029
+ # Messaging gives a member's link as /in/ACoA… (their member id), not
2030
+ # the /in/name link your connections list has, so their name comes
2031
+ # along to match them up (the app keeps the match, not the name).
2032
+ key = _link_key(v.get("profileUrl"))
2033
+ if key:
2034
+ name = f"{text_of(v.get('firstName'))} {text_of(v.get('lastName'))}".strip()
2035
+ if name:
2036
+ names[key] = name
1969
2037
  stack.extend(v.values())
1970
2038
  elif isinstance(v, list):
1971
2039
  stack.extend(v)
@@ -1984,7 +2052,13 @@ def voyager_conversations(payload):
1984
2052
  who |= people(v)
1985
2053
  if who:
1986
2054
  unread = x.get("unreadCount")
1987
- out.append({"people": sorted(who), "at": int(at), "unread": int(unread) if isinstance(unread, (int, float)) else None})
2055
+ link = x.get("conversationUrl")
2056
+ group = x.get("groupChat")
2057
+ out.append({"people": sorted(who), "at": int(at), "unread": int(unread) if isinstance(unread, (int, float)) else None,
2058
+ "names": {u: names[u] for u in who if u in names},
2059
+ "url": link if isinstance(link, str) and link.startswith("https://www.linkedin.com/messaging/") else None,
2060
+ "group": group if isinstance(group, bool) else None,
2061
+ "latest": latest_of(x)})
1988
2062
  for v in x.values():
1989
2063
  walk(v)
1990
2064
  elif isinstance(x, list):
@@ -1997,8 +2071,9 @@ def voyager_conversations(payload):
1997
2071
  def sync_messages(headless=False):
1998
2072
  """Experimental: read your LinkedIn messages list once, for the Social tab. Opens
1999
2073
  Messaging in your own Chrome, scrolls the list a few times, and keeps from LinkedIn's
2000
- own data only who each 1:1 conversation is with, when it was last active and whether
2001
- it has unread messages. No search budget; no message text is read or kept."""
2074
+ own data who each 1:1 conversation is with, when it was last active, whether it has
2075
+ unread messages, its link and who wrote last. The newest message's words go to the
2076
+ app only while Keep my messages is on there. No search budget."""
2002
2077
  from playwright.sync_api import sync_playwright
2003
2078
  kept = []
2004
2079
  with sync_playwright() as p:
@@ -2021,7 +2096,12 @@ def sync_messages(headless=False):
2021
2096
  if stop_requested() or _window_closed(page):
2022
2097
  break
2023
2098
  try:
2024
- page.evaluate("""() => { const l = document.querySelector('.msg-conversations-container__conversations-list, [class*="conversations-list"]');
2099
+ # The list scrolls inside its own panel, not the window: find the panel
2100
+ # that holds the conversation links and scroll that to its end.
2101
+ page.evaluate("""() => {
2102
+ let l = document.querySelector('.msg-conversations-container__conversations-list, [class*="conversations-list"]');
2103
+ const a = document.querySelector('a[href*="/messaging/thread/"], li[class*="conversation-listitem"]');
2104
+ for (let e = a; !l && e; e = e.parentElement) if (e.scrollHeight > e.clientHeight + 20) l = e;
2025
2105
  if (l) l.scrollTop = l.scrollHeight; }""")
2026
2106
  except Exception:
2027
2107
  pass
@@ -2036,7 +2116,35 @@ def sync_messages(headless=False):
2036
2116
  _wire_sample(data, prefix="messages")
2037
2117
  convos.extend(voyager_conversations(data))
2038
2118
  browser.close()
2039
- # You are the one in the most conversations; keep the 1:1 ones.
2119
+ _me, people = live_people(convos)
2120
+ print(f" Found {len(convos)} conversations in LinkedIn's data, {len(people)} with one person.")
2121
+ # The newest message's words go to the app only if Keep my messages is on
2122
+ # there. The app checks too; this way they don't even leave the scanner.
2123
+ keep = False
2124
+ try:
2125
+ r = requests.get(f"{APP_URL}/api/social", headers=app_headers(json_body=False), timeout=15)
2126
+ keep = r.ok and ((r.json() or {}).get("social") or {}).get("keepMessages") is True
2127
+ except Exception:
2128
+ pass
2129
+ if not keep:
2130
+ for v in people.values():
2131
+ if v.get("preview"):
2132
+ v["preview"]["text"] = None
2133
+ try:
2134
+ r = requests.put(f"{APP_URL}/api/social", json={"live": people}, headers=app_headers(), timeout=30)
2135
+ got = r.json() if r.ok else {}
2136
+ matched, unmatched = got.get("people", 0), got.get("unmatched", 0)
2137
+ print(f" Saved for the Social tab: {matched} matched to your connections, {unmatched} not.")
2138
+ except Exception as e:
2139
+ print(f" Couldn't save them: {e}")
2140
+
2141
+
2142
+ def live_people(convos):
2143
+ """The messages sync's conversations, per person: you are the one in the most
2144
+ conversations, and only the 1:1 ones are kept. For each person, their newest
2145
+ conversation's last activity, unread count, name (for the app to match them to
2146
+ a connection; it keeps the match, not the name), link, who wrote last and the
2147
+ newest message. Returns (you, {profile link: ...})."""
2040
2148
  counts = {}
2041
2149
  for c in convos:
2042
2150
  for u in c["people"]:
@@ -2045,17 +2153,23 @@ def sync_messages(headless=False):
2045
2153
  people = {}
2046
2154
  for c in convos:
2047
2155
  others = [u for u in c["people"] if u != me]
2048
- if len(others) != 1:
2156
+ if len(others) != 1 or c.get("group") is True:
2049
2157
  continue
2050
2158
  cur = people.get(others[0])
2051
- if not cur or c["at"] > cur["last"]:
2052
- people[others[0]] = {"last": c["at"], "unread": c["unread"]}
2053
- print(f" Found {len(convos)} conversations in LinkedIn's data, {len(people)} with one person.")
2054
- try:
2055
- requests.put(f"{APP_URL}/api/social", json={"live": people}, headers=app_headers(), timeout=30)
2056
- print(" Saved for the Social tab.")
2057
- except Exception as e:
2058
- print(f" Couldn't save them: {e}")
2159
+ if cur and c["at"] <= cur["last"]:
2160
+ continue
2161
+ entry = {"last": c["at"], "unread": c["unread"], "name": c.get("names", {}).get(others[0]),
2162
+ "threadUrl": c.get("url"), "lastFromThem": None, "preview": None}
2163
+ latest = c.get("latest")
2164
+ # Who sent it is known only when their link is one of the two in the
2165
+ # conversation; a link in some other form says nothing either way.
2166
+ sender = latest.get("from") if latest else None
2167
+ if sender in (me, others[0]):
2168
+ from_me = sender == me
2169
+ entry["lastFromThem"] = not from_me
2170
+ entry["preview"] = {"t": latest["t"], "fromMe": from_me, "text": latest.get("text")}
2171
+ people[others[0]] = entry
2172
+ return me, people
2059
2173
 
2060
2174
 
2061
2175
  def _drip_wait(label, seconds):
@@ -2095,9 +2209,28 @@ def _drip_before_search():
2095
2209
  if not _drip_wait("Today's searches are used", _seconds_until_search_frees()):
2096
2210
  return False
2097
2211
  continue
2212
+ wait = _auto_ceiling_wait()
2213
+ if wait:
2214
+ label, seconds = wait
2215
+ if not _drip_wait(label, seconds):
2216
+ return False
2217
+ continue
2098
2218
  return True
2099
2219
 
2100
2220
 
2221
+ def _auto_ceiling_wait(now=None):
2222
+ """Auto scan's own ceilings (AUTO_DAY_CAP in 24 hours, AUTO_WEEK_CAP in 7 days):
2223
+ None to go ahead, or (why, seconds until the oldest counted search drops out)."""
2224
+ now = now if now is not None else time.time()
2225
+ searches = sorted(t for t in _read_activity()["searches"] if now - 7 * DAY_SECONDS < t <= now)
2226
+ day = [t for t in searches if now - DAY_SECONDS < t]
2227
+ if len(day) >= AUTO_DAY_CAP:
2228
+ return (f"Auto scan's {AUTO_DAY_CAP} searches for today are used", day[-AUTO_DAY_CAP] + DAY_SECONDS - now + 5)
2229
+ if len(searches) >= AUTO_WEEK_CAP:
2230
+ return (f"Auto scan's {AUTO_WEEK_CAP} searches for this week are used", searches[-AUTO_WEEK_CAP] + 7 * DAY_SECONDS - now + 5)
2231
+ return None
2232
+
2233
+
2101
2234
  def interruptible_sleep(seconds, on_tick=None, step=5):
2102
2235
  """Sleep, but notice a stop request. Returns False if we were interrupted."""
2103
2236
  waited = 0
@@ -4073,7 +4206,7 @@ Examples:
4073
4206
  parser.add_argument("--rescrape", type=str, help="Delete + re-scrape a bridge's cluster from scratch")
4074
4207
  parser.add_argument("--company", type=str, help="Scan everyone the app can see at one company")
4075
4208
  parser.add_argument("--messages", action="store_true",
4076
- help="Experimental: read your messages list once for the Social tab (who, when, unread; never text)")
4209
+ help="Experimental: read your messages list once for the Social tab (who, when, unread; the newest message's words only if Keep my messages is on)")
4077
4210
  parser.add_argument("--experimental", action="store_true",
4078
4211
  help="Auto-Bridge, experimental: all-day pacing (sittings, rests, daytime only, "
4079
4212
  "waits for the daily budget) and reading LinkedIn's own data beside the page text")