~callum/beavers-dam

599e29a19c78a90a95933ad3019da629a63269e6 — Callum Brown 1 year, 27 days ago 4021a62
v0.5: webRequests refactor

Listen in background for requests to dammed pages and redirect to dam
8 files changed, 122 insertions(+), 106 deletions(-)

A background.js
M dam/dam.html
M dam/dam.js
M journal/journal.js
M manifest.json
M options/options.js
M popup/popup.js
M todo
A background.js => background.js +43 -0
@@ 0,0 1,43 @@
const damURL = chrome.runtime.getURL("dam/dam.html")
const extensionOrigin = new URL(damURL).origin;

async function dam(requestDetails) {
	if (requestDetails.initiator === extensionOrigin) {
		await chrome.storage.local.set({ dammedURL: null });
	} else {
		await chrome.storage.local.set({ dammedURL: requestDetails.url });
		const [tab] = await chrome.tabs.query({ active: true});
		await chrome.tabs.update(
			tab.id,
			{ url: damURL },
		);
	}
}


async function updateListener(dammed) {
	await chrome.webRequest.onBeforeRequest.removeListener(dam);
	// Don't use an empty urls Array or dam will execute on *every* request
	if (dammed.length > 0) {
		await chrome.webRequest.onBeforeRequest.addListener(
		  dam,
		  { urls: dammed, types: ["main_frame"] }
		);
	}
}


async function messageListener(request, sender, sendResponse) {
	if (request.updatedDammed) {
		await updateListener(request.updatedDammed);
	}
}


async function main() {
	const items = await chrome.storage.sync.get({ dammed: [] });
	await updateListener(items.dammed);
	await chrome.runtime.onMessage.addListener(messageListener);
}

main();

M dam/dam.html => dam/dam.html +0 -5
@@ 9,11 9,6 @@
		<textarea id="entry"></textarea>
		<br>
		<button id="save-and-view-journal">Save &amp; View Journal</button>
		<br>
		<label for="domain">Domain to continue to</label>
		<br>
		<input id="domain"></input>
		<br>
		<button id="save-and-continue">Save &amp; Continue</button>
	</body>
</html>

M dam/dam.js => dam/dam.js +6 -6
@@ 1,9 1,11 @@
async function save(text) {
	let items = await chrome.storage.local.get({ entries: [] });
	const items = await chrome.storage.local.get(
		{ entries: [], dammedURL: null}
	);
	// Can't store a Map, use an Object instead
	const entry = {
		"date": Date(),
		"hostname": location.hostname,
		"hostname": new URL(items.dammedURL).hostname,
		"text": text,
	};
	items.entries.push(entry);


@@ 21,15 23,13 @@ async function saveAndViewJournal() {


async function saveAndContinue() {
	// XXX: Does not validate domain input
	const domain = document.getElementById("domain").value;
	const destination = new URL(`https://${domain}`);
	const text = document.getElementById("entry").value;
	const numWords = text.trim().split(" ").length;
	const items = await chrome.storage.sync.get({ minWords: 1 });
	if (text.length > 0 && numWords >= items.minWords) {
		await save(text);
		window.open(destination, "_self");
		const localitems = await chrome.storage.local.get({ dammedURL: null });
		window.open(localitems.dammedURL, "_self");
	} else {
		window.alert(`Please write at least ${items.minWords} words.`);
	}

M journal/journal.js => journal/journal.js +11 -14
@@ 1,31 1,28 @@
let container = document.getElementById("entries");
const container = document.getElementById("entries");


const addEntry = (entry) => {
	let entryDiv = document.createElement("div");
function addEntry(entry) {
	const entryDiv = document.createElement("div");
	container.append(entryDiv);	

	let date = document.createElement("p");
	const date = document.createElement("p");
	date.textContent = entry.date;
	entryDiv.append(date);

	let hostname = document.createElement("p");
	const hostname = document.createElement("p");
	hostname.textContent = entry.hostname;
	entryDiv.append(hostname);

	let text = document.createElement("p");
	const text = document.createElement("p");
	text.textContent = entry.text;
	entryDiv.append(text);
};
}


const loadEntries = () => {
	chrome.storage.local.get(
		{ entries: [] }
	).then((items) => {
		items.entries.forEach(addEntry);
	});
};
async function loadEntries() {
	items = await chrome.storage.local.get({ entries: [] });
	items.entries.forEach(addEntry);
}


document.addEventListener("DOMContentLoaded", loadEntries);

M manifest.json => manifest.json +5 -2
@@ 2,15 2,18 @@
	"manifest_version": 3,
	"name": "Beaver's Dam",
	"description": "Base Level Extension",
	"version": "0.4",
	"version": "0.5",
	"permissions": [
		"storage",
		"tabs",
		"declarativeNetRequestWithHostAccess"
		"webRequest"
	],
	"host_permissions": [
		"<all_urls>"
	],
	"background": {
		"service_worker": "background.js"
	},
	"action": {
		"default_popup": "popup/popup.html"
	},

M options/options.js => options/options.js +29 -29
@@ 1,3 1,19 @@
const minWordsInput = document.getElementById("min-words");
const importInput = document.getElementById("import");


async function loadOptions() {
	const items = await chrome.storage.sync.get({ minWords: 1 });
	minWordsInput.value = items.minWords;
}


async function saveOptions() {
	// TODO: type check before saving
	await chrome.storage.sync.set({ minWords: minWordsInput.value });
}


async function exportData() {
	const data = JSON.stringify({
		sync: await chrome.storage.sync.get(null),


@@ 11,30 27,12 @@ async function exportData() {
	a.remove();
};

const minWordsInput = document.getElementById("min-words");

// Load saved options
chrome.storage.sync.get(
	{ minWords: 0 }
).then((items) => {
	minWordsInput.value = items.minWords;
});

document.getElementById("save").addEventListener("click", () => {
	// TODO: type check before saving
	chrome.storage.sync.set(
		{ minWords: minWordsInput.value }
	);
});

document.getElementById("export").addEventListener("click", exportData);

const importInput = document.getElementById("import");
importInput.addEventListener("change", () => {
async function importData() {
	const [file] = importInput.files;
	if (file) {
		const reader = new FileReader();
		reader.addEventListener("load", () => {
		reader.addEventListener("load", async () => {
			let data;
			let parsed = false;
			try {


@@ 48,15 46,17 @@ importInput.addEventListener("change", () => {
			// only contain primative values, `Array`s, `Date`s, and `Regex`s,
			// which can be serialised by the Chrome Storage API.
			// See docs for StorageArea.set
			chrome.storage.sync.set(data["sync"]).then(
				() => chrome.storage.local.set(data["local"]).then(
					() => {
						window.alert("Import successful");
						location.reload();
					}
				)
			);
			await chrome.storage.sync.set(data["sync"]);
			await chrome.storage.local.set(data["local"]);
			window.alert("Import successful");
			location.reload();
		});
		reader.readAsText(file);
	}
});
}


document.getElementById("save").addEventListener("click", saveOptions);
document.getElementById("export").addEventListener("click", exportData);
importInput.addEventListener("change", importData);
loadOptions();

M popup/popup.js => popup/popup.js +23 -43
@@ 1,53 1,30 @@
async function updateRules(domains) {
	await chrome.declarativeNetRequest.updateDynamicRules({
		addRules: [{
			id: 1,
			action: {
				type: "redirect",
				redirect: {extensionPath: "/dam/dam.html"},
			},
			condition: {
				requestDomains: domains,
				resourceTypes: ["main_frame"],
				excludedInitiatorDomains: [
					new URL(chrome.runtime.getURL("dam/dam.html")).hostname
				],
			},
		}],
		removeRuleIds: [1],
	});
}


async function setToggleDammedBehaviour() {
	const toggleDammedButton= document.getElementById("toggle-dammed");
	const [tab] = await chrome.tabs.query({ active: true});
	const domain = new URL(tab.url).hostname;
	const url = new URL(tab.url);
	const match_pattern = url.origin + "/*";

	const [rule] = await chrome.declarativeNetRequest.getDynamicRules(
		{ ruleIds: [1] }
	);

	let dammed;
	if (rule === undefined) {
		// No rules set yet
		dammed = [];
	} else {
		dammed = rule.condition.requestDomains;
	}
	const items = await chrome.storage.sync.get({ dammed: []});
	let dammed = items.dammed;

	if (dammed.includes(domain)) {
		toggleDammedButton.textContent = `Undam ${domain}`;
		const index = dammed.indexOf(domain);
	let toggleDammed;
	if (dammed.includes(match_pattern)) {
		toggleDammedButton.textContent = `Undam ${url.hostname}`;
		const index = dammed.indexOf(match_pattern);
		dammed.splice(index, 1);
	} else {
		toggleDammedButton.textContent = `Dam ${domain}`;
		dammed.push(domain);
		toggleDammedButton.textContent = `Dam ${url.hostname}`;
		dammed.push(match_pattern);
	}

	toggleDammedButton.addEventListener("click", async () => {
		await updateRules(dammed);
		chrome.tabs.reload(tab.id).then(window.close);
		await chrome.storage.sync.set({ dammed: dammed });
		// Get background.js to update onBeforeRequest listener
		await chrome.runtime.sendMessage(
			{ updatedDammed: dammed }
		);
		await chrome.tabs.reload(tab.id);
		window.close();
	});
}



@@ 64,9 41,12 @@ document.getElementById("open-options").addEventListener(

document.getElementById("clear").addEventListener(
	"click",
	() => {
		chrome.storage.sync.clear();
		chrome.storage.local.clear();
	async () => {
		await chrome.storage.sync.clear();
		await chrome.storage.local.clear();
		await chrome.runtime.sendMessage(
			{ updatedDammed: [] }
		);
	},
);


M todo => todo +5 -7
@@ 1,15 1,13 @@
programmatically modify matching pages in background.js?

Custom prompt (different prompt for undamming?)

Custom style (colours, font)

Different prompt, word count per domain?

Make it work with youtube
	- dam doesn't overlay everything with default z-index
	- media autoplays
	- takes ages to load
Type check before saving options


Fix dammedURL being overwritten when dam for one site is left
and in the meantime another site is dammed.

Type check before saving options
Forbid damming the extension