// ==UserScript== // @name data extraction linkedin // @namespace Violentmonkey Scripts // @match https://www.linkedin.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_getValues // @grant GM_setValues // @grant GM_listValues // @grant GM_deleteValue // @grant GM_deleteValues // @grant GM_addStyle // @grant GM_addElement // @version 0.1 // @author Siarhei Siniak // @license Unlicense // @description 10/08/2024, 8:44:59 PM // @inject-into document // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@1 // @require https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js // @noframes // ==/UserScript== /* Use this extension to disalbe CSP for linkedin https://addons.mozilla.org/en-US/firefox/addon/header-editor/ https://github.com/FirefoxBar/HeaderEditor https://github.com/violentmonkey/violentmonkey/issues/1335 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src { "request": [], "sendHeader": [], "receiveHeader": [ { "enable": true, "name": "disable CSP for linkedin", "ruleType": "modifyReceiveHeader", "matchType": "domain", "pattern": "www.linkedin.com", "exclude": "", "group": "Ungrouped", "isFunction": false, "action": { "name": "content-security-policy", "value": "" } } ], "receiveBody": [] } */ class Linkedin { constructor() { this.data = new Map(); this.ui = { root: null, entries: null, }; this.state = { search: '', }; } async data_load() { let self = this; const keys = await GM_listValues(); let loaded = 0; for (let o of keys) { if (!o.startsWith('data-')) { return; } self.data.set( o.slice(5,), await GM_getValue(o) ); loaded += 1; } console.log({action: 'loaded', total: loaded}); } string_reduce (text) { return text.replaceAll(/\s+/gi, ' ').trim(); } parse_header() { let self = this; return [ $( '.scaffold-finite-scroll__content > div > .relative .update-components-header' ).map((i, o) => ({ header: o.innerText })), $( '.scaffold-finite-scroll__content > div > .relative .update-components-actor' ).map((i, o) => { let header = $(o); let teaser = $(o).parents('.relative') .parent().find('.feed-shared-update-v2__description-wrapper'); return { header: self.string_reduce(header.text()), teaser: self.string_reduce(teaser.text()), }; }) ] } async data_add (entry) { let self = this; if (self.data.has(entry.header)) { return false; } self.data.set(entry.header, { entry: entry, ts: (new Date()).valueOf(), }); await GM_setValue( 'data-' + entry.header, self.data.get(entry.header) ) console.log('saved ' + entry.header); console.log(self.data.get(entry.header)); return true; } async document_on_changed () { let self = this; let state_changed = false; if ( JSON.stringify(self.state_get()) != JSON.stringify(self.state) ) { state_changed = true; self.old_state = self.state; self.state = self.state_get(); } let current_data = self.parse_header(); let changed = false; for (let o of current_data[0]) { let current_changed = await self.data_add(o); if (current_changed) { changed = current_changed; } } for (let o of current_data[1]) { let current_changed = await self.data_add(o); if (current_changed) { changed = current_changed; } } if ( changed || ( state_changed || self.ui.entries === null && self.data.size > 0 ) ) { self.display(); } } listener_add() { let self = this; return VM.observe( document.body, () => { self.document_on_changed(); } ); } display_init() { let self = this; self.ui.root = $(`<div class=online-fxreader-linkedin>`); $('head').append($('<style>').html(` div.online-fxreader-linkedin { height: 2em; overflow: scroll; z-index: 9999; position: fixed; top: 5em; background: yellow; margin-left: 1em; word-wrap: anywhere; white-space: break-spaces; margin-right: 1em; width: calc(100% - 2em); } .d-none { display: none !important; }; .online-fxreader-linkedin pre { white-space: wrap; word-break: break-all; } .online-fxreader-linkedin:hover { height: 10em; } `)); GM_addElement('script', { "textContent": ` class Linkedin { constructor() { let self = this; this.ui = { root: () => { return document.getElementsByClassName('online-fxreader-linkedin')[0]; }, }; self.ui.search = () => { let search = self.ui.root().getElementsByClassName('search')[0]; let search_input = search.getElementsByTagName('input')[0]; return search_input; }; self.ui.state = () => { let state = self.ui.root().getElementsByClassName('state')[0]; return state; }; } blah(class_name) { console.log('blah'); Array.from( document.getElementsByClassName(class_name) ).forEach((o) => o.remove()); } state_update(partial) { let self = this; let ui_state = self.ui.state(); let old_state = JSON.parse(ui_state.innerText); ui_state.innerText = JSON.stringify( { ...old_state, ...partial } ); } search_on_change() { let self = this; let search = self.ui.search(); self.state_update( { search: search.value } ); } }; const online_fxreader_linkedin = new Linkedin(); console.log('started'); ` }); $(document.body).append(self.ui.root); } state_get() { let self = this; if (self.ui.state && self.ui.state.text() !== '') { return JSON.parse(self.ui.state.text()); } else { return {}; } } state_set(partial) { let self = this; self.ui.state.text( { ...state_get(), ...partial } ); } display() { let self = this; let sorted_entries = Array.from(self.data.entries()).sort( (a, b) => a[1].ts - b[1].ts ); self.ui.root.empty(); let search = $('<div>').addClass('search').append( $('<input>').val(self.state.search) ).attr( 'onkeyup', `online_fxreader_linkedin.search_on_change()`, ); self.ui.root.append(search); self.ui.state = $('<div>').addClass('state d-none').text( JSON.stringify(self.state) ); self.ui.root.append(self.ui.state); //state_set(old_state); let entries = $('<div>').addClass('entries'); for (let o of sorted_entries.reverse()) { let raw = JSON.stringify(o[1]); let ts = (new Date(o[1].ts)); let entry = $('<div>').addClass('entry'); entry.append( $('<div>').addClass('ts').text( ts.toISOString(), ) ); entry.append( $('<div>').addClass('header').text( o[1].entry.header ) ); entry.append( $('<div>').addClass('teaser').text( o[1].entry.teaser ) ); // entry.append($('<pre>').text(raw)); entries.append(entry); } self.ui.entries = entries; self.ui.root.append(entries); GM_addElement('script', { "class": 'bridge', "textContent": ` online_fxreader_linkedin.blah('bridge'); ` }); } } const l = new Linkedin(); (async () => { await l.data_load(); const disconnect = l.listener_add(); l.display_init(); })();