mirror of
https://github.com/bellingcat/auto-archiver-extension.git
synced 2026-06-08 03:28:34 +03:00
new features
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -5,6 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"http-status-codes": "^2.2.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"materialize-css": "^1.0.0-rc.2",
|
"materialize-css": "^1.0.0-rc.2",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
@@ -4442,6 +4443,11 @@
|
|||||||
"entities": "^3.0.1"
|
"entities": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-status-codes": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="
|
||||||
|
},
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
@@ -12165,6 +12171,11 @@
|
|||||||
"entities": "^3.0.1"
|
"entities": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http-status-codes": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="
|
||||||
|
},
|
||||||
"human-signals": {
|
"human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"extends": "stylelint-config-xo"
|
"extends": "stylelint-config-xo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"http-status-codes": "^2.2.0",
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"materialize-css": "^1.0.0-rc.2",
|
"materialize-css": "^1.0.0-rc.2",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.2.45",
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
"sourceDir": "distribution",
|
"sourceDir": "distribution",
|
||||||
"run": {
|
"run": {
|
||||||
"startUrl": [
|
"startUrl": [
|
||||||
"https://github.com/fregante/browser-extension-template"
|
"https://github.com/bellingcat/auto-archiver-extension"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
min-width: 40em;
|
min-width: 45em;
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,17 +12,17 @@ body {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#archive-results .row {
|
table.archive-results .row {
|
||||||
/* table-layout: fixed; */
|
/* table-layout: fixed; */
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* #archive-results td {
|
/* .archive-results td {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#archive-results td:nth-child(2) {
|
.archive-results td:nth-child(2) {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!doctype html>
|
<!-- <!doctype html>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Options</title>
|
<title>Options</title>
|
||||||
|
|
||||||
@@ -33,4 +33,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script src="../js/options.js" type="module"></script>
|
<script src="../js/options.js" type="module"></script> -->
|
||||||
|
|||||||
@@ -1,64 +1,142 @@
|
|||||||
|
|
||||||
// Import './options-storage.js';
|
// Import './options-storage.js';
|
||||||
import optionsStorage from './options-storage.js';
|
import optionsStorage from './options-storage.js';
|
||||||
|
import { getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
// TODO: stable ID https://developer.chrome.com/docs/extensions/mv3/tut_oauth/
|
// TODO: stable ID https://developer.chrome.com/docs/extensions/mv3/tut_oauth/
|
||||||
const API_ENDPOINT = 'http://localhost:8004/tasks'
|
const API_ENDPOINT = 'http://localhost:8004/tasks'
|
||||||
// const API_ENDPOINT = 'http://134.122.58.133:8004/tasks';
|
// const API_ENDPOINT = 'http://134.122.58.133:8004/tasks';
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(((r, s, sR) => {
|
const LOGIN_FAILED = `Could not login, make sure your google account email has been granted access by the developers.`;
|
||||||
processMessages(r, s, sR);
|
|
||||||
|
chrome.runtime.onMessage.addListener(((r, s, sendResponse) => {
|
||||||
|
processMessages(r, s)
|
||||||
|
//TODO: improve body
|
||||||
|
.then(response => {
|
||||||
|
console.log(`SUCCESS (${r.action}): ${JSON.stringify(response)}`)
|
||||||
|
sendResponse({ status: "success", result: response })
|
||||||
|
}
|
||||||
|
).catch(error => {
|
||||||
|
let message = error.message == "Failed to fetch" ? `Unable to call the API: ${error.message}` : error.message;
|
||||||
|
console.log(`ERROR (${r.action}): ${error}`)
|
||||||
|
setErrorMessage(message);
|
||||||
|
sendResponse({ status: "error", result: message })
|
||||||
|
});
|
||||||
return true; // Needed for sendResponse to be async
|
return true; // Needed for sendResponse to be async
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function processMessages(request, sender, sendResponse) {
|
function processMessages(request, sender) {
|
||||||
console.info(`action {${request.action}} from ${sender.tab ? 'content-script (' + sender.tab.url + ')' : 'the extension'}`);
|
|
||||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
console.info(`action {${request.action}} from ${sender.tab ? 'content-script (' + sender.tab.url + ')' : 'the extension'}`);
|
||||||
|
|
||||||
switch (request.action) {
|
switch (request.action) {
|
||||||
case 'archive': {
|
case 'archive': {
|
||||||
archiveUrl(sendResponse, accessToken);
|
archiveUrl(resolve, reject, request.optionalUrl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'search': {
|
case 'search': {
|
||||||
const tasks = await search(request.query, accessToken);
|
search(resolve, reject, request.query);
|
||||||
sendResponse(tasks);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'status': {
|
case 'status': {
|
||||||
const taskDb = await getTaskById(request.task.task_id);
|
const taskDb = await getTaskById(request.task.id);
|
||||||
if (taskDb?.status === 'SUCCESS' || taskDb?.status === 'FAILURE'|| taskDb?.status === 'REVOKED') {
|
if (taskDb?.status === 'SUCCESS' || taskDb?.status === 'FAILURE' || taskDb?.status === 'REVOKED') {
|
||||||
console.log('ALREADY FINSIHED, NO REQS');
|
console.log('ALREADY FINISHED, NO REQS');
|
||||||
sendResponse(taskDb);
|
resolve(taskDb);
|
||||||
} else {
|
} else {
|
||||||
const taskFresh = await checkTaskStatus(request.task, accessToken);
|
resolve(await checkTaskStatus(resolve, reject, request.task));
|
||||||
sendResponse(taskFresh);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'getTasks': {
|
case 'getTasks': {
|
||||||
sendResponse(await getAllTasks());
|
resolve(await getAllTasks());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'syncLocalTasks': {
|
||||||
|
syncLocalTasks(resolve, reject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'getErrorMessage': {
|
||||||
|
resolve(await getErrorMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'setErrorMessage': {
|
||||||
|
await setErrorMessage(request.errorMessage)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'getCurrentUrl': {
|
||||||
|
getUrl(resolve, reject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'getProfileEmail': {
|
||||||
|
getProfileEmail(resolve, reject);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'oauthLogin': {
|
||||||
|
oauthLogin(resolve, reject);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// No default
|
// No default
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function archiveUrl(sendResponse, accessToken) {
|
|
||||||
|
function getUrl(resolve, reject) {
|
||||||
chrome.tabs.query({
|
chrome.tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
lastFocusedWindow: true,
|
lastFocusedWindow: true,
|
||||||
}, async tabs => {
|
}, async tabs => {
|
||||||
const url = tabs[0].url;
|
const url = tabs[0].url;
|
||||||
console.log(`url=${url}`);
|
resolve(url);
|
||||||
const response = await searchTask(url, accessToken);
|
|
||||||
const newArchive = { url, task_id: response.task_id, status: 'PENDING', result: {} };
|
|
||||||
await upsertTask(newArchive);
|
|
||||||
sendResponse(newArchive);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchTask(url, accessToken) {
|
function getProfileEmail(resolve, reject) {
|
||||||
|
chrome.identity.getProfileUserInfo({ accountStatus: 'ANY' }, async userInfo => {
|
||||||
|
resolve(userInfo);
|
||||||
|
//TODO: reject if bad user info?
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function oauthLogin(resolve, reject) {
|
||||||
|
try {
|
||||||
|
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||||
|
console.error(`GOT token ${accessToken}`);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// reject(new Error(`LOGIN FAILED: ${e}`));
|
||||||
|
console.error(`LOGIN FAILED: ${e}`);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveUrl(resolve, reject, optionalUrl) {
|
||||||
|
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||||
|
if (accessToken == undefined) {
|
||||||
|
reject(new Error(LOGIN_FAILED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chrome.tabs.query({
|
||||||
|
active: true,
|
||||||
|
lastFocusedWindow: true,
|
||||||
|
}, async tabs => {
|
||||||
|
console.warn(optionalUrl)
|
||||||
|
const url = optionalUrl || tabs[0].url;
|
||||||
|
console.log(`url=${url}`);
|
||||||
|
submitUrlArchive(url, accessToken).then(async response => {
|
||||||
|
const newArchive = { url, id: response.id, status: 'PENDING', result: {} };
|
||||||
|
await upsertTask(newArchive);
|
||||||
|
resolve(newArchive);
|
||||||
|
}).catch(e => reject(e));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitUrlArchive(url, accessToken) {
|
||||||
console.log('API: SUBMIT');
|
console.log('API: SUBMIT');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch(API_ENDPOINT, {
|
fetch(API_ENDPOINT, {
|
||||||
@@ -67,72 +145,128 @@ function searchTask(url, accessToken) {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ url, access_token: accessToken }),
|
body: JSON.stringify({ url, access_token: accessToken }),
|
||||||
}).then(
|
})
|
||||||
response => response.json(),
|
.then(getJsonOrError)
|
||||||
).then(response => resolve(response),
|
.then(response => resolve(response))
|
||||||
).catch(error => {
|
.catch(e => reject(e));
|
||||||
console.log(`There was an error: ${error}`);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTaskStatus(task, accessToken) {
|
function checkTaskStatus(resolve, reject, task) {
|
||||||
console.log('API: STATUS');
|
console.log('API: STATUS');
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((InnerResolve, innerReject) => {
|
||||||
fetch(`${API_ENDPOINT}/${task.task_id}?` + new URLSearchParams({ access_token: accessToken }), {
|
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||||
method: 'GET',
|
if (accessToken == undefined) {
|
||||||
headers: {
|
reject(new Error(LOGIN_FAILED));
|
||||||
'Content-Type': 'application/json',
|
return;
|
||||||
},
|
}
|
||||||
}).then(
|
fetch(`${API_ENDPOINT}/${task.id}?` + new URLSearchParams({ access_token: accessToken }), {
|
||||||
response => response.json(),
|
method: 'GET',
|
||||||
).then(response => {
|
headers: {
|
||||||
const new_task = {
|
'Content-Type': 'application/json',
|
||||||
url: task.url,
|
},
|
||||||
task_id: response.task_id,
|
})
|
||||||
status: response.task_status,
|
.then(getJsonOrError)
|
||||||
result: typeof response.task_result == "object" ? response.task_result : JSON.parse(response.task_result),
|
.then(response => {
|
||||||
};
|
const new_task = {
|
||||||
console.log(`status ${new_task.url}: ${new_task.task_id}`);
|
url: task.url,
|
||||||
upsertTask(new_task);
|
id: response.id,
|
||||||
resolve(new_task);
|
status: response.status,
|
||||||
},
|
result: typeof response.result == "object" ? response.result : JSON.parse(response.result),
|
||||||
).catch(error => reject(error));
|
};
|
||||||
|
console.log(`status ${new_task.url}: ${new_task.id}`);
|
||||||
|
(async () => {
|
||||||
|
await upsertTask(new_task);
|
||||||
|
InnerResolve(new_task);
|
||||||
|
})();
|
||||||
|
})
|
||||||
|
.catch(e => reject(e));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function search(query, accessToken) {
|
function search(resolve, reject, url) {
|
||||||
console.log('API: SEARCH');
|
console.log('API: SEARCH');
|
||||||
return new Promise((resolve, reject) => {
|
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||||
fetch(`${API_ENDPOINT}/search?` + new URLSearchParams({ access_token: accessToken, query }), {
|
if (accessToken == undefined) {
|
||||||
|
reject(new Error(LOGIN_FAILED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`${API_ENDPOINT}/search-url?` + new URLSearchParams({ access_token: accessToken, url }), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
}).then(
|
})
|
||||||
response => response.json(),
|
.then(getJsonOrError)
|
||||||
).then(response => resolve(response),
|
.then(jsonResponse => { resolve(jsonResponse.map(t => { t.status = "SUCCESS"; return t; })) })
|
||||||
).catch(error => {
|
.catch(e => reject(e));
|
||||||
console.log(`There was an error: ${error}`);
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncLocalTasks(resolve, reject) {
|
||||||
|
console.log('API: SYNC');
|
||||||
|
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||||
|
if (accessToken == undefined) {
|
||||||
|
reject(new Error(LOGIN_FAILED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`${API_ENDPOINT}/sync?` + new URLSearchParams({ access_token: accessToken }), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(getJsonOrError)
|
||||||
|
.then(async cloudTasks => {
|
||||||
|
const storage = await optionsStorage.getAll();
|
||||||
|
cloudTasks.forEach(cTask => {
|
||||||
|
storage.archivedUrls[cTask.id] = {
|
||||||
|
url: cTask.url,
|
||||||
|
id: cTask.id,
|
||||||
|
status: "SUCCESS",
|
||||||
|
result: typeof cTask.result == "object" ? cTask.result : JSON.parse(cTask.result),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
await optionsStorage.set(storage);
|
||||||
|
resolve(storage.archivedUrls)
|
||||||
|
})
|
||||||
|
.catch(e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJsonOrError(response) {
|
||||||
|
let additionalErrorInfo = "";
|
||||||
|
if (response.status == 401) additionalErrorInfo = `Check that this email has been granted permission.`;
|
||||||
|
if (response.status != 200) throw new Error(`${response.status}: ${getReasonPhrase(response.status)} ${additionalErrorInfo}`);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
async function getAllTasks() {
|
async function getAllTasks() {
|
||||||
const storage = await optionsStorage.getAll();
|
const storage = await optionsStorage.getAll();
|
||||||
|
console.log(`OP: GET_ALL, has ${Object.keys(storage.archivedUrls).length} entries`)
|
||||||
return storage.archivedUrls;
|
return storage.archivedUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: improve with less reads from storage
|
|
||||||
async function upsertTask(task) {
|
async function upsertTask(task) {
|
||||||
const storage = await optionsStorage.getAll();
|
const storage = await optionsStorage.getAll();
|
||||||
storage.archivedUrls[task.task_id] = task;
|
storage.archivedUrls[task.id] = task;
|
||||||
await optionsStorage.set(storage);
|
await optionsStorage.set(storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTaskById(task) {
|
async function getTaskById(task) {
|
||||||
const storage = await optionsStorage.getAll();
|
const storage = await optionsStorage.getAll();
|
||||||
return storage.archivedUrls[task.task_id];
|
return storage.archivedUrls[task.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getErrorMessage() {
|
||||||
|
const storage = await optionsStorage.getAll();
|
||||||
|
console.log(`OP: GET_ERROR_MESSAGE has '${storage.errorMessage}'`)
|
||||||
|
return storage.errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setErrorMessage(errorMessage) {
|
||||||
|
const storage = await optionsStorage.getAll();
|
||||||
|
storage.errorMessage = errorMessage;
|
||||||
|
await optionsStorage.set(storage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import OptionsSync from 'webext-options-sync';
|
|||||||
export default new OptionsSync({
|
export default new OptionsSync({
|
||||||
defaults: {
|
defaults: {
|
||||||
archivedUrls: {},
|
archivedUrls: {},
|
||||||
|
errorMessage: ""
|
||||||
},
|
},
|
||||||
migrations: [
|
migrations: [
|
||||||
OptionsSync.migrations.removeUnused,
|
// OptionsSync.migrations.removeUnused,
|
||||||
],
|
],
|
||||||
logging: true,
|
logging: true,
|
||||||
|
storageType: "local"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
// eslint-disable-next-line import/no-unassigned-import
|
// eslint-disable-next-line import/no-unassigned-import
|
||||||
import 'webext-base-css';
|
// import 'webext-base-css';
|
||||||
import '../css/options.css';
|
// import '../css/options.css';
|
||||||
|
|
||||||
import optionsStorage from './options-storage.js';
|
// import optionsStorage from './options-storage.js';
|
||||||
|
|
||||||
const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')];
|
// const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')];
|
||||||
const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')];
|
// const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')];
|
||||||
const output = document.querySelector('.color-output');
|
// const output = document.querySelector('.color-output');
|
||||||
|
|
||||||
function updateOutputColor() {
|
// function updateOutputColor() {
|
||||||
output.style.backgroundColor = `rgb(${rangeInputs[0].value}, ${rangeInputs[1].value}, ${rangeInputs[2].value})`;
|
// output.style.backgroundColor = `rgb(${rangeInputs[0].value}, ${rangeInputs[1].value}, ${rangeInputs[2].value})`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function updateInputField(event) {
|
// function updateInputField(event) {
|
||||||
numberInputs[rangeInputs.indexOf(event.currentTarget)].value = event.currentTarget.value;
|
// numberInputs[rangeInputs.indexOf(event.currentTarget)].value = event.currentTarget.value;
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (const input of rangeInputs) {
|
// for (const input of rangeInputs) {
|
||||||
input.addEventListener('input', updateOutputColor);
|
// input.addEventListener('input', updateOutputColor);
|
||||||
input.addEventListener('input', updateInputField);
|
// input.addEventListener('input', updateInputField);
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function init() {
|
// async function init() {
|
||||||
await optionsStorage.syncForm('#options-form');
|
// await optionsStorage.syncForm('#options-form');
|
||||||
updateOutputColor();
|
// updateOutputColor();
|
||||||
}
|
// }
|
||||||
|
|
||||||
init();
|
// init();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {createApp} from 'vue';
|
import { createApp } from 'vue';
|
||||||
import Popup from '../vue/Popup.vue';
|
import Popup from '../vue/Popup.vue';
|
||||||
import 'materialize-css/dist/css/materialize.min.css';
|
import 'materialize-css/dist/css/materialize.min.css';
|
||||||
import 'material-design-icons/iconfont/material-icons.css';
|
import 'material-design-icons/iconfont/material-icons.css';
|
||||||
@@ -12,6 +12,7 @@ app.mount('#app');
|
|||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
// TODO: uncomment if using options
|
// TODO: uncomment if using options
|
||||||
// listenForOptionsClick();
|
// listenForOptionsClick();
|
||||||
|
M.Tooltip.init(document.querySelectorAll('.tooltipped'), { enterDelay: 500 }); // enable tooltips
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function listenForOptionsClick() {
|
// Function listenForOptionsClick() {
|
||||||
|
|||||||
19
source/js/utils.js
Normal file
19
source/js/utils.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// export async function callBackground(parameters) {
|
||||||
|
// try {
|
||||||
|
// const answer = await chrome.runtime.sendMessage(parameters);
|
||||||
|
// if (answer.status == "error") {
|
||||||
|
// console.error(`error: ${answer.result}`)
|
||||||
|
// //TODO: modal/errors
|
||||||
|
// return null;
|
||||||
|
// } else {
|
||||||
|
// return answer.result;
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"128": "img/ben-archiver.png"
|
"128": "img/ben-archiver.png"
|
||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage", "tabs", "identity"
|
"storage", "tabs", "identity", "identity.email"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<p v-if="!login">please <a v-on:click="oauthLogin" href="#">login</a> into your google account</p>
|
||||||
|
<div v-if="errorMessage.length" class="red darken-1 white-text">Error: {{ errorMessage }}</div>
|
||||||
<h5>
|
<h5>
|
||||||
<img src="../img/ben-archiver.png" alt="icon" id="icon">
|
<img src="../img/ben-archiver.png" alt="icon" id="icon">
|
||||||
auto-archiver extension
|
auto-archiver extension
|
||||||
<button v-on:click="archive" class="waves-effect waves-light btn-small right">Archive!</button>
|
<button v-on:click="archive" class="tooltipped waves-effect waves-light btn-small right" data-position="bottom"
|
||||||
<!-- <button v-on:click="searchF" class="waves-effect waves-light btn-small right">SEARCH</button> -->
|
data-tooltip="Archive this URL">
|
||||||
|
<i class="material-icons left">cloud</i> Archive!</button>
|
||||||
|
<button v-on:click="checkArchive"
|
||||||
|
class="tooltipped waves-effect waves-light btn-small right flat light-blue darken-3" style="margin-right:10px;"
|
||||||
|
data-position="bottom" data-tooltip="Check if this URL has been archived">lookup URL</button>
|
||||||
</h5>
|
</h5>
|
||||||
|
<!-- <label><input type="checkbox" v-model="takeScreenshot" /><span>take screenshot</span></label> -->
|
||||||
<div class="input-field col s6">
|
<div class="input-field col s6">
|
||||||
<i class="material-icons prefix">search</i>
|
<i class="material-icons prefix">search</i>
|
||||||
<input id="icon_prefix" type="text" v-model="search">
|
<input id="icon_prefix" type="text" ref="search" v-model="search" v-on:input="searchTasks">
|
||||||
<label for="icon_prefix">Search for URLs</label>
|
<label for="icon_prefix">Search for URLs</label>
|
||||||
</div>
|
</div>
|
||||||
<table id="archive-results">
|
<table class="archive-results" v-if="localTasksShownLength > 0 || onlineTasksLength > 0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="row">
|
<tr class="row">
|
||||||
<th class="col s1"></th>
|
<th class="col s1"></th>
|
||||||
@@ -20,12 +27,25 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<TaskItem v-for="t in displayTasks" :key="t.task_id" :initial-task="t" />
|
<TaskItem v-for="t in displayTasks" :key="t.id" :initial-task="t" taskType="local" />
|
||||||
|
<TaskItem v-for="t in onlineTasks" :key="t.id" :initial-task="t" taskType="online" />
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<div v-if="noSearchResults">
|
||||||
|
No results... do you want to <a v-on:click="archiveFromSearch" href="#">archive</a>?
|
||||||
|
</div>
|
||||||
|
<p v-show="login">
|
||||||
|
<a href="#" v-on:click="syncLocalTasks" class="tooltipped"
|
||||||
|
data-tooltip="updates local database with entries submitted by the current user" data-position="top">Sync</a>
|
||||||
|
my cloud archives.
|
||||||
|
</p>
|
||||||
|
<small>
|
||||||
|
<span v-if="login">Hello {{ login }}!</span>
|
||||||
|
<span class="right"><a href="https://github.com/bellingcat/auto-archiver-extension/issues" target="_blank">Issue
|
||||||
|
tracker</a></span>
|
||||||
|
</small>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import M from 'materialize-css';
|
import M from 'materialize-css';
|
||||||
import TaskItem from './TaskItem.vue';
|
import TaskItem from './TaskItem.vue';
|
||||||
@@ -33,59 +53,153 @@ import TaskItem from './TaskItem.vue';
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tasks: [],
|
login: false,
|
||||||
isLoading: false,
|
tasks: {},
|
||||||
search: ''
|
onlineTasks: [],
|
||||||
|
isSearchingOnline: false,
|
||||||
|
search: '',
|
||||||
|
errorMessage: ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
archive: function () {
|
archive: function () {
|
||||||
// M.toast({html: 'DONE'})
|
|
||||||
|
|
||||||
// chrome.tabs.sendMessage
|
|
||||||
this.isLoading = !this.isLoading;
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await this.callBackground({ action: "archive" });
|
||||||
action: "archive"
|
if (!response) return;
|
||||||
});
|
|
||||||
this.url = response.url;
|
this.url = response.url;
|
||||||
this.task_id = response.task_id;
|
this.id = response.id;
|
||||||
this.addTask(response)
|
this.addTask(response)
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
searchF: function(){
|
archiveFromSearch: function () {
|
||||||
|
//TODO: how to deduplicate? calling archive(this.search) is bad because of default injections into archive
|
||||||
(async () => {
|
(async () => {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await this.callBackground({ action: "archive", optionalUrl: this.search });
|
||||||
action: "search",
|
if (!response) return;
|
||||||
query: "search query"
|
this.url = response.url;
|
||||||
});
|
this.id = response.id;
|
||||||
console.log(response)
|
this.addTask(response)
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
checkArchive: function () {
|
||||||
|
(async () => {
|
||||||
|
const response = await this.callBackground({ action: "getCurrentUrl" });
|
||||||
|
if (!response) return;
|
||||||
|
this.search = response;
|
||||||
|
this.$refs.search.focus();
|
||||||
|
this.searchTasks();
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
displayAllTasks: function () {
|
displayAllTasks: function () {
|
||||||
(async () => {
|
(async () => {
|
||||||
const tasks = await chrome.runtime.sendMessage({
|
const response = await this.callBackground({ action: "getTasks" });
|
||||||
action: "getTasks"
|
if (!response) return;
|
||||||
});
|
this.tasks = response;
|
||||||
console.log(tasks)
|
})();
|
||||||
|
},
|
||||||
|
syncLocalTasks: function () {
|
||||||
|
(async () => {
|
||||||
|
console.log("SYNC")
|
||||||
|
const tasks = await this.callBackground({ action: "syncLocalTasks" });
|
||||||
|
console.log(`TASKS: ${JSON.stringify(tasks)}`)
|
||||||
|
if (!tasks) return;
|
||||||
this.tasks = tasks;
|
this.tasks = tasks;
|
||||||
|
M.toast({ html: `sync complete: ${this.localTasksLength} task${this.localTasksLength != 1 ? 's' : ''} available`, classes: "green accent-4" });
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
displayLogin: function () {
|
||||||
|
(async () => {
|
||||||
|
const response = await this.callBackground({ action: "getProfileEmail" });
|
||||||
|
if (!response) {
|
||||||
|
this.login = false;
|
||||||
|
} else {
|
||||||
|
this.login = response.email;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
clearErrorMessage: function () {
|
||||||
|
setTimeout(async () => {
|
||||||
|
this.errorMessage = "";
|
||||||
|
await this.callBackground({ action: "setErrorMessage", errorMessage: this.errorMessage });
|
||||||
|
}, 3000)
|
||||||
|
},
|
||||||
|
displayErrorMessage: function () {
|
||||||
|
(async () => {
|
||||||
|
this.errorMessage = await this.callBackground({ action: "getErrorMessage" });
|
||||||
|
this.clearErrorMessage()
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
oauthLogin: function () {
|
||||||
|
(async () => {
|
||||||
|
const loginSuccessful = await this.callBackground({ action: "oauthLogin" });
|
||||||
|
if (loginSuccessful === null) return;
|
||||||
|
M.toast({ html: loginSuccessful ? "login success" : "login failed", classes: loginSuccessful ? "green accent-4" : "red darken-1" });
|
||||||
|
if (loginSuccessful) { this.displayLogin(); }
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
addTask: function (task) {
|
addTask: function (task) {
|
||||||
this.tasks[task.task_id] = task;
|
this.tasks[task.id] = task;
|
||||||
|
},
|
||||||
|
searchTasks: function () {
|
||||||
|
console.log(`searching tasks? ${!this.isSearchingOnline}`);
|
||||||
|
if (this.isSearchingOnline) {
|
||||||
|
console.log(`skipping search, another is still active`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.search.length <= 3) {
|
||||||
|
this.onlineTasks = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(async () => {
|
||||||
|
this.isSearchingOnline = true; {
|
||||||
|
const onlineTasks = await this.callBackground({ action: "search", query: this.search });
|
||||||
|
if (!onlineTasks) return;
|
||||||
|
this.onlineTasks = (onlineTasks || []).filter(id => !Object.keys(this.tasks).includes(id))
|
||||||
|
}
|
||||||
|
this.isSearchingOnline = false;
|
||||||
|
})();
|
||||||
|
},
|
||||||
|
callBackground: async function (parameters) {
|
||||||
|
try {
|
||||||
|
const answer = await chrome.runtime.sendMessage(parameters);
|
||||||
|
if (answer.status == "error") {
|
||||||
|
console.error(`showing error to user: ${JSON.stringify(answer.result)}`)
|
||||||
|
M.toast({ html: `Error: ${answer.result}`, classes: "red darken-1", completeCallback: this.clearErrorMessage })
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return answer.result;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (parameters.action == "search") this.isSearchingOnline = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
displayTasks() {
|
displayTasks() {
|
||||||
let st = Object.values(this.tasks)
|
return Object.values(this.tasks)
|
||||||
.filter(t => t?.url.toLowerCase().includes(this.search.toLowerCase()))
|
.filter(t => t?.url.toLowerCase().includes(this.search.toLowerCase()))
|
||||||
.sort((t1, t2) => (t1?.result?._processed_at || 0) - (t2?.result?._processed_at || 0)).slice(0, 25)
|
.sort((t1, t2) => (t1?.result?._processed_at || 0) - (t2?.result?._processed_at || 0)).slice(0, 25)
|
||||||
return st
|
},
|
||||||
}
|
noSearchResults() {
|
||||||
|
return this.search.length > 3 && !this.isSearchingOnline && Object.keys(this.onlineTasks).length == 0;
|
||||||
|
},
|
||||||
|
localTasksShownLength() {
|
||||||
|
return Object.keys(this.displayTasks).length > 0;
|
||||||
|
},
|
||||||
|
localTasksLength() {
|
||||||
|
return Object.keys(this.tasks).length;
|
||||||
|
},
|
||||||
|
onlineTasksLength() {
|
||||||
|
return Object.keys(this.onlineTasks).length > 0;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
M.AutoInit()
|
M.AutoInit();
|
||||||
this.displayAllTasks()
|
this.displayAllTasks();
|
||||||
|
this.displayLogin();
|
||||||
|
this.displayErrorMessage();
|
||||||
},
|
},
|
||||||
created() { },
|
created() { },
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<tr class="row">
|
<tr class="row">
|
||||||
<td class="col s1">
|
<td class="col s1">
|
||||||
<div v-if="task.status == 'PENDING'" class="preloader-wrapper small active">
|
<div v-if="taskPending" class="preloader-wrapper small active">
|
||||||
<div class="spinner-layer ">
|
<div class="spinner-layer ">
|
||||||
<div class="circle-clipper left">
|
<div class="circle-clipper left">
|
||||||
<div class="circle"></div>
|
<div class="circle"></div>
|
||||||
@@ -14,24 +14,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="task.status == 'SUCCESS'">
|
<div v-if="taskSucceeded">
|
||||||
<i class="material-icons small green-text darken-4">done</i>
|
<i v-if="taskType == 'online'" title="found on the cloud"
|
||||||
|
class="material-icons small green-text darken-4">cloud_done</i>
|
||||||
|
<i v-if="taskType == 'local'" title="found locally"
|
||||||
|
class="material-icons small green-text darken-4">done</i>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="task.status == 'FAILURE' || task.status == 'REVOKED'">
|
<div v-if="taskFailed">
|
||||||
<i class="material-icons small red-text darken-4">clear</i>
|
<i class="material-icons small red-text darken-4">clear</i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="col s5"><a :href="task?.url" target="_blank">{{ task.url }}</a></td>
|
<td class="col s5"><a :href="task?.url" target="_blank">{{ task.url }}</a></td>
|
||||||
<td class="col s2"><a v-if="archiveUrl.length" :href="archiveUrl" target="_blank">{{ task?.result?.status || "open"
|
<td class="col s2"><a v-if="archiveUrl.length" :href="archiveUrl" target="_blank">{{ task?.result?.status || "open"
|
||||||
}}</a> </td>
|
}}</a></td>
|
||||||
<td class="col s3">{{ readbleDate }}</td>
|
<td class="col s3">{{ readbleDate }}</td>
|
||||||
|
<td class="col s1" v-if="(taskFailed || taskSucceeded) && taskType == 'local'">
|
||||||
|
<a class="delete-btn" href="#" v-on:click="deleteTask"><i class="material-icons small">delete</i></a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
<style>
|
||||||
|
.delete-btn {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'TaskItem',
|
name: 'TaskItem',
|
||||||
props: ['initialTask'],
|
props: ['initialTask', 'taskType'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
task: this.initialTask
|
task: this.initialTask
|
||||||
@@ -39,37 +54,55 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkStatus: function () {
|
checkStatus: function () {
|
||||||
console.log(this.task)
|
console.log(`Checking status ${JSON.stringify(this.task)}`);
|
||||||
if (this.taskFinished(this.task)) return
|
if (this.taskFinished(this.task)) return
|
||||||
this.intervalId = setInterval(function () {
|
this.intervalId = setInterval(function () {
|
||||||
chrome.runtime.sendMessage({
|
this.callBackground(
|
||||||
action: "status",
|
{ action: "status", task: this.task }
|
||||||
task: this.task
|
).then(updated_task => {
|
||||||
}).then(updated_task => {
|
|
||||||
console.log(updated_task)
|
|
||||||
if (this.taskFinished(updated_task)) {
|
if (this.taskFinished(updated_task)) {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
this.task = updated_task
|
this.task = updated_task
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}.bind(this), 2500);
|
}.bind(this), 2500);
|
||||||
},
|
},
|
||||||
taskFinished: function (task) {
|
taskFinished: function (task) {
|
||||||
return task.status == 'SUCCESS' || task.status == 'FAILURE' || task.status == 'REVOKED';
|
return task.status == 'SUCCESS' || task.status == 'FAILURE' || task.status == 'REVOKED';
|
||||||
|
},
|
||||||
|
callBackground: async function (parameters) {
|
||||||
|
try {
|
||||||
|
const answer = await chrome.runtime.sendMessage(parameters);
|
||||||
|
if (answer.status == "error") {
|
||||||
|
console.error(`error: ${answer.result}`)
|
||||||
|
//TODO: modal/errors
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return answer.result;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
archiveUrl() {
|
archiveUrl() {
|
||||||
// return this.task?.result?.media?.urls.at(0) || '';
|
|
||||||
console.log(this.task?.result?.media);
|
|
||||||
console.log(this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media"));
|
|
||||||
console.log(this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media")?.urls?.at(0));
|
|
||||||
return this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media")?.at(0)?.urls?.at(0) || '';
|
return this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media")?.at(0)?.urls?.at(0) || '';
|
||||||
},
|
},
|
||||||
readbleDate() {
|
readbleDate() {
|
||||||
if (this.task?.result?._processed_at) {
|
if (this.task?.result?._processed_at) {
|
||||||
return new Date(this.task.result._processed_at * 1e3).toISOString().slice(0, 19);
|
return new Date(this.task.result._processed_at * 1e3).toISOString().slice(0, 19);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
taskPending() {
|
||||||
|
return this.task.status == 'PENDING';
|
||||||
|
},
|
||||||
|
taskSucceeded() {
|
||||||
|
return this.task.status == 'SUCCESS';
|
||||||
|
},
|
||||||
|
taskFailed() {
|
||||||
|
return !this.taskSucceeded && !this.taskPending;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
Reference in New Issue
Block a user