diff --git a/extension/background.js b/extension/background.js new file mode 100644 index 0000000..ecb9908 --- /dev/null +++ b/extension/background.js @@ -0,0 +1,128 @@ +var browser = browser || chrome; + + +var overrides = null; + +var accepted = false; +var installationId = null; +browser.storage.local.get(['overrides', 'accepted', 'installationId'], v => { + accepted = v.accepted + overrides = v.overrides || {} + if(!v.installationId){ + installationId = (Math.random()+ '.' +Math.random() + '.' +Math.random()).replace(/\./g, ''); + browser.storage.local.set({installationId: installationId}); + } +}) + +var bloomFilters = []; + +function loadBloomFilter(name) { + + var url = browser.extension.getURL('data/' + name + '.dat'); + fetch(url).then(response => { + response.arrayBuffer().then(arrayBuffer => { + var array = new Uint32Array(arrayBuffer); + var b = new BloomFilter(array, 17); + b.name = name; + bloomFilters.push(b); + }); + }); +} + + + +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + if(message.acceptClicked !== undefined) { + accepted = message.acceptClicked; + browser.storage.local.set({accepted: accepted}); + browser.tabs.remove(sender.tab.id); + if(accepted && uncommittedResponse) + saveLabel(uncommittedResponse) + uncommittedResponse = null; + return; + } + var response = {}; + var transphobic = message.myself && bloomFilters.filter(x => x.name == 'transphobic')[0].test(message.myself); + for (var id of message.ids) { + if(transphobic){ + if(id == message.myself) continue; + var sum = 0; + for(var i = 0; i < id.length; i++){ + sum += id.charCodeAt(i); + } + if(sum % 8 != 0) continue; + } + if (overrides[id] !== undefined) response[id] = overrides[id] + else { + for (var bloomFilter of bloomFilters) { + if (bloomFilter.test(id)) response[id] = bloomFilter.name; + } + } + } + sendResponse(response); +}); + +loadBloomFilter('transphobic'); +loadBloomFilter('t-friendly'); + + + +function createContextMenu(text, id) { + browser.contextMenus.create({ + id: id, + title: text, + contexts: ["link"], + targetUrlPatterns: [ + "*://*.facebook.com/*", + "*://*.youtube.com/*", + "*://*.reddit.com/*", + "*://*.twitter.com/*" + ] + }); +} + +createContextMenu('Mark as anti-trans', 'mark-transphobic'); +createContextMenu('Mark as t-friendly', 'mark-t-friendly'); +createContextMenu('Clear', 'mark-none'); +createContextMenu('Help', 'help'); + +var uncommittedResponse = null; + +function saveLabel(response){ + if(accepted){ + overrides[response.identifier] = response.mark; + browser.storage.local.set({overrides: overrides}) + //console.log(response); + browser.tabs.sendMessage(response.tabId, { updateAllLabels: true }); + //browser.tabs.executeScript(response.tabId, {code: 'updateAllLabels()'}); + return; + } + uncommittedResponse = response; + openHelp(); +} + +function openHelp(){ + browser.tabs.create({ + url: browser.extension.getURL('help.html') + }) +} + +browser.contextMenus.onClicked.addListener(function (info, tab) { + if(info.menuItemId == 'help'){ + openHelp(); + return; + } + + var label = info.menuItemId.substring('mark-'.length); + if(label == 'none') label = ''; + browser.tabs.sendMessage(tab.id, { + mark: label, + url: info.linkUrl, + elementId: info.targetElementId + }, null, response => { + if (!response.identifier) return; + response.tabId = tab.id; + saveLabel(response); + }) + +}); diff --git a/extension/content.css b/extension/content.css new file mode 100644 index 0000000..88c8869 --- /dev/null +++ b/extension/content.css @@ -0,0 +1,6 @@ +.assigned-label-transphobic { color: #991515 !important; } +.assigned-label-t-friendly { color: #77B91E !important; } +.assigned-label-bad { color: #A87200 !important; } +.assigned-label-good { color: #014E0B !important; } + +.has-assigned-label * { color: inherit !important; } diff --git a/extension/content.js b/extension/content.js new file mode 100644 index 0000000..0f3f5c0 --- /dev/null +++ b/extension/content.js @@ -0,0 +1,312 @@ +var browser = browser || chrome; + +var hostname = typeof (location) != 'undefined' ? location.hostname : ''; +if (hostname.startsWith('www.')) { + hostname = hostname.substring(4); +} +if (hostname.endsWith('.reddit.com')) hostname = 'reddit.com'; + +var myself = null; +if (hostname == 'reddit.com') { + myself = document.querySelector('#header-bottom-right .user a'); + if (!myself) { + var m = document.querySelector('#USER_DROPDOWN_ID'); + if (m) { + m = [...m.querySelectorAll('*')].filter(x => x.childNodes.length == 1 && x.firstChild.nodeType == 3).map(x => x.textContent)[0] + if (m) myself = 'reddit.com/user/' + m; + } + } +} +if (hostname == 'facebook.com') { + var m = document.querySelector("[id^='profile_pic_header_']") + if (m) myself = 'facebook.com/' + m.id.match(/header_(\d+)/)[1]; +} +if (hostname == 'twitter.com') { + myself = document.querySelector('.DashUserDropdown-userInfo a'); +} + +if (myself && (myself.href || myself.startsWith('http:') || myself.startsWith('https:'))) + myself = getIdentifier(myself); +//console.log('Myself: ' + myself) + +function init() { + updateAllLabels(); + + var observer = new MutationObserver(mutationsList => { + + for (var mutation of mutationsList) { + if (mutation.type == 'childList') { + for (var node of mutation.addedNodes) { + if (node.tagName == 'A') { + initLink(node); + } + if (node.querySelectorAll) { + for (var subnode of node.querySelectorAll('a')) { + initLink(subnode); + } + } + } + } + } + solvePendingLabels(); + + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + + +} + +function updateAllLabels(refresh) { + if (refresh) knownLabels = {}; + var links = document.links; + for (var i = 0; i < links.length; i++) { + var a = links[i]; + initLink(a); + } + solvePendingLabels(); +} + + +var knownLabels = {}; + +var labelsToSolve = []; +function solvePendingLabels() { + if (!labelsToSolve.length) return; + var uniqueIdentifiers = Array.from(new Set(labelsToSolve.map(x => x.identifier))); + var tosolve = labelsToSolve; + labelsToSolve = []; + browser.runtime.sendMessage({ ids: uniqueIdentifiers, myself: myself }, response => { + for (item of tosolve) { + var label = response[item.identifier]; + knownLabels[item.identifier] = label || ''; + applyLabel(item.element, item.identifier); + } + }); +} + +function applyLabel(a, identifier) { + + if (a.assignedCssLabel) { + a.classList.remove('assigned-label-' + a.assignedCssLabel); + a.classList.remove('has-assigned-label'); + } + var label = a.assignedLabel = knownLabels[identifier] || ''; + + // https://rationalwiki.org/wiki/RationalWiki:Webshites + a.assignedCssLabel = + !label ? '' : + label == 'liked' ? 'liked' : + label == 'disliked' ? 'disliked' : + label == 'rw-radfem' || label == 'terf' || label == 'transphobic' || label == 'anti-lgbt' ? 'transphobic' : + label == 't-friendly' ? 't-friendly' : + label == 'rw-skeptic' || label == 'rw-liberal' || label == 'rw-feminism' || label == 'science' ? 'good' : + 'bad'; + if (a.assignedCssLabel) { + a.classList.add('assigned-label-' + a.assignedCssLabel); + a.classList.add('has-assigned-label'); + } +} + +function initLink(a) { + var identifier = getIdentifier(a); + if (!identifier) return; + + var label = knownLabels[identifier]; + if (label === undefined) { + labelsToSolve.push({ element: a, identifier: identifier }); + return; + } + applyLabel(a, identifier); +} + +var currentlySelectedEntity = null; + +function isHostedOn(/** @type {string}*/fullHost, /** @type {string}*/baseHost) { + if (baseHost.length > fullHost.length) return false; + if (baseHost.length == fullHost.length) return baseHost == fullHost; + var k = fullHost.charCodeAt(fullHost.length - baseHost.length - 1); + if (k == 0x2E) return fullHost.endsWith(baseHost); + else return false; +} + +function getQuery(/** @type {string}*/search) { + if (!search) return {}; + var s = {}; + if (search.startsWith('?')) search = search.substring(1); + for (var pair of search.split('&')) { + var z = pair.split('='); + if (z.length != 2) continue; + s[decodeURIComponent(z[0]).replace(/\+/g, ' ')] = decodeURIComponent(z[1].replace(/\+/g, ' ')); + } + return s; +} + +function takeFirstPathComponents(/** @type {string}*/path, /** @type {number}*/num) { + var m = path.split('/') + m = m.slice(1, 1 + num); + if (m.length && !m[m.length - 1]) m.length--; + return '/' + m.join('/'); +} + +function getCurrentFacebookPageId() { + + // page + var elem = document.querySelector("a[rel=theater][aria-label='Profile picture']"); + if (elem) { + var p = elem.href.match(/facebook\.com\/(\d+)/) + if (p) return p[1]; + } + + // page (does not work if page is loaded directly) + elem = document.querySelector("[ajaxify^='/page_likers_and_visitors_dialog']") + if (elem) return elem.getAttribute('ajaxify').match(/\/(\d+)\//)[1]; + + // group + elem = document.querySelector("[id^='headerAction_']"); + if (elem) return elem.id.match(/_(\d+)/)[1]; + + // profile + elem = document.querySelector('#pagelet_timeline_main_column'); + if (elem && elem.dataset.gt) return JSON.parse(elem.dataset.gt).profile_owner; + return null; +} + +function getIdentifier(urlstr) { + if (!urlstr) return null; + + if (hostname == 'facebook.com') { + var parent = urlstr.parentElement; + if (parent && (parent.tagName == 'H1' || parent.id == 'fb-timeline-cover-name')) { + var id = getCurrentFacebookPageId(); + //console.log('Current fb page: ' + id) + if (id) + return 'facebook.com/' + id; + } + if (urlstr.dataset) { + var hovercard = urlstr.dataset.hovercard; + if (hovercard) { + var id = hovercard.match(/id=(\d+)/)[1]; + if (id) + return 'facebook.com/' + id; + } + var gt = urlstr.dataset.gt; + if (gt) { + var gtParsed = JSON.parse(gt); + if (gtParsed.engagement && gtParsed.engagement.eng_tid) { + return 'facebook.com/' + gtParsed.engagement.eng_tid; + } + } + var p = urlstr; + while (p) { + var bt = p.dataset.bt; + if (bt) { + var btParsed = JSON.parse(bt); + if (btParsed.id) return 'facebook.com/' + btParsed.id; + } + p = p.parentElement; + } + } + } + if (urlstr.href !== undefined) urlstr = urlstr.href; + if (!urlstr) return null; + if (urlstr.endsWith('#')) return null; + try { + var url = new URL(urlstr); + } catch (e) { + return null; + } + if (url.protocol != 'http:' && url.protocol != 'https:') return null; + + if (url.href.indexOf('http', 1) != -1) { + var s = getQuery(url.search); + urlstr = null; + for (var key in s) { + if (s.hasOwnProperty(key)) { + var element = s[key]; + if (element.startsWith('http:') || element.startsWith('https')) { + urlstr = element; + break; + } + } + } + if (urlstr == null) { + urlstr = url.href.substring(url.href.indexOf('http', 1)) + } + try { + url = new URL(urlstr); + } catch (e) { } + } + + var host = url.hostname; + if (isHostedOn(host, 'web.archive.org')) { + var match = url.href.match(/\/web\/\w+\/(.*)/); + if (!match) return null; + url = new URL('http://' + match[1]); + host = url.hostname; + } + if (host.startsWith('www.')) host = host.substring(4); + + if (isHostedOn(host, 'facebook.com')) { + var s = getQuery(url.search); + var p = url.pathname.replace('/pg/', '/'); + return 'facebook.com/' + (s.story_fbid || s.set || s.story_fbid || s._ft_ || s.ft_id || s.id || takeFirstPathComponents(p, p.startsWith('/groups/') ? 2 : 1).substring(1)); + } + if (isHostedOn(host, 'reddit.com')) { + return 'reddit.com' + takeFirstPathComponents(url.pathname.replace('/u/', '/user/'), 2).toLowerCase(); + } + if (isHostedOn(host, 'twitter.com')) { + return 'twitter.com' + takeFirstPathComponents(url.pathname, 1).toLowerCase() + } + if (isHostedOn(host, 'youtube.com')) { + return 'youtube.com' + takeFirstPathComponents(url.pathname, 2); + } + if (host.indexOf('.blogspot.') != -1) { + return host.match(/([a-zA-Z0-9\-]*)\.blogspot/)[1].toLowerCase() + '.blogspot.com'; + } + + var id = host; + if (id.startsWith('www.')) id = id.substr(4); + if (id.startsWith('m.')) id = id.substr(2); + return id.toLowerCase(); +} + + +init(); + +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + + if (message.updateAllLabels) { + updateAllLabels(true); + return; + } + message.contextPage = window.location.href; + var target = message.elementId ? browser.menus.getTargetElement(message.elementId) : null; + + //console.log(message.url) + var links = target ? [target] : [...document.getElementsByTagName('A')].filter(x => x.href == message.url) + + //if (!links.length) console.log('Already empty :(') + var identifier = links.length ? getIdentifier(links[0]) : getIdentifier(message.url); + if (!identifier) return; + var snippets = links.map(node => { + + while (node) { + var classList = node.classList; + if (hostname == 'facebook.com' && node.dataset && node.dataset.ftr) return node; + if (hostname == 'reddit.com' && (classList.contains('scrollerItem') || classList.contains('thing') || classList.contains('Comment'))) return node; + if (hostname == 'twitter.com' && (classList.contains('stream-item'))) return node; + node = node.parentElement; + } + //console.log('Reached the top without a satisfying element') + return null; + }) + snippets = snippets.filter((item, pos) => item && snippets.indexOf(item) == pos); + message.identifier = identifier; + message.snippets = snippets.filter((item, pos) => pos <= 10).map(x => x.outerHTML); + sendResponse(message); +}) \ No newline at end of file diff --git a/extension/help.js b/extension/help.js new file mode 100644 index 0000000..f0dc3ff --- /dev/null +++ b/extension/help.js @@ -0,0 +1,10 @@ +var browser = browser || chrome; + +document.getElementById('cancelButton').addEventListener('click', () => { + + browser.runtime.sendMessage({ acceptClicked: false }, response => { }); +}) +document.getElementById('acceptButton').addEventListener('click', () => { + + browser.runtime.sendMessage({ acceptClicked: true }, response => { }); +}) \ No newline at end of file