browser-extension/extension/background.js
2024-11-26 10:05:58 +01:00

308 lines
7.8 KiB
JavaScript

/*
extension background script listening for events
*/
'use strict';
console.log('running background.js');
let browserType = getBrowser();
// boilerplate to dedect browser type api
function getBrowser() {
if (typeof chrome !== 'undefined') {
if (typeof browser !== 'undefined') {
return browser;
} else {
return chrome;
}
} else {
console.log('failed to detect browser');
throw 'browser detection error';
}
}
// send get request to API backend
async function sendGet(path) {
let access = await getAccess();
const url = `${access.url}:${access.port}/${path}`;
console.log('GET: ' + url);
const rawResponse = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Token ' + access.apiKey,
mode: 'no-cors',
},
});
const content = await rawResponse.json();
return content;
}
// send post/put request to API backend
async function sendData(path, payload, method) {
let access = await getAccess();
const url = `${access.url}:${access.port}/${path}`;
console.log(`${method}: ${url}`);
console.log(`${method}: ${JSON.stringify(payload)}`);
try {
const rawResponse = await fetch(url, {
method: method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Token ' + access.apiKey,
mode: 'no-cors',
},
body: JSON.stringify(payload),
});
const content = await rawResponse.json();
return content;
} catch (e) {
console.error(e);
return null;
}
}
// read access details from storage.local
async function getAccess() {
let storage = await browserType.storage.local.get('access');
return storage.access;
}
// check if cookie is valid
async function getCookieState() {
const path = 'api/cookie/';
let response = await sendGet(path);
console.log('cookie state: ' + JSON.stringify(response));
return response;
}
// send ping to server
async function verifyConnection() {
const path = 'api/ping/';
let message = await sendGet(path);
console.log('verify connection: ' + JSON.stringify(message));
if (message?.response === 'pong') {
return true;
} else if (message?.detail) {
throw new Error(message.detail);
} else {
throw new Error(`got unknown message ${JSON.stringify(message)}`);
}
}
// send youtube link from injected buttons
async function download(url) {
let apiURL = 'api/download/';
let autostart = await browserType.storage.local.get('autostart');
if (Object.keys(autostart).length > 0 && autostart.autostart.checked) {
apiURL += '?autostart=true';
}
return await sendData(
apiURL,
{
data: [
{
youtube_id: url,
status: 'pending',
},
],
},
'POST'
);
}
async function subscribe(url, subscribed) {
return await sendData(
'api/channel/',
{
data: [
{
channel_id: url,
channel_subscribed: subscribed,
},
],
},
'POST'
);
}
async function videoExists(id) {
const path = `api/video/${id}/`;
let response = await sendGet(path);
if (!response.data) return false;
let access = await getAccess();
return new URL(`video/${id}/`, `${access.url}:${access.port}/`).href;
}
async function getChannelCache() {
let cache = await browserType.storage.local.get('cache');
if (cache.cache) return cache;
return { cache: {} };
}
async function setChannel(channelHandler, channelId) {
let cache = await getChannelCache();
cache.cache[channelHandler] = { id: channelId, timestamp: Date.now() };
browserType.storage.local.set(cache);
}
async function getChannelId(channelHandle) {
let cache = await getChannelCache();
if (cache.cache[channelHandle]) {
return cache.cache[channelHandle]?.id;
}
let channel = await searchChannel(channelHandle);
if (channel) setChannel(channelHandle, channel.channel_id);
return channel.channel_id;
}
async function searchChannel(channelHandle) {
const path = `api/channel/search/?q=${channelHandle}`;
let response = await sendGet(path);
return response.data;
}
async function getChannel(channelHandle) {
let channelId = await getChannelId(channelHandle);
if (!channelId) return;
const path = `api/channel/${channelId}/`;
let response = await sendGet(path);
return response.data;
}
async function cookieStr(cookieLines) {
const path = 'api/cookie/';
let payload = {
cookie: cookieLines.join('\n'),
};
let response = await sendData(path, payload, 'PUT');
return response;
}
function buildCookieLine(cookie) {
// 2nd argument controls subdomains, and must match leading dot in domain
let includeSubdomains = cookie.domain.startsWith('.') ? 'TRUE' : 'FALSE';
return [
cookie.domain,
includeSubdomains,
cookie.path,
cookie.httpOnly.toString().toUpperCase(),
Math.trunc(cookie.expirationDate) || 0,
cookie.name,
cookie.value,
].join('\t');
}
async function sendCookies() {
console.log('function sendCookies');
const acceptableDomains = ['.youtube.com', 'youtube.com', 'www.youtube.com'];
let cookieStores = await browserType.cookies.getAllCookieStores();
let cookieLines = [
'# Netscape HTTP Cookie File',
'# https://curl.haxx.se/rfc/cookie_spec.html',
'# This is a generated file! Do not edit.\n',
];
for (let i = 0; i < cookieStores.length; i++) {
const cookieStore = cookieStores[i];
let allCookiesStore = await browserType.cookies.getAll({
domain: '.youtube.com',
storeId: cookieStore['id'],
});
for (let j = 0; j < allCookiesStore.length; j++) {
const cookie = allCookiesStore[j];
if (acceptableDomains.includes(cookie.domain)) {
cookieLines.push(buildCookieLine(cookie));
}
}
}
let response = cookieStr(cookieLines);
return response;
}
/*
process and return message if needed
the following messages are supported:
type Message =
| { type: 'verify' }
| { type: 'cookieState' }
| { type: 'sendCookie' }
| { type: 'download', url: string }
| { type: 'subscribe', url: string }
| { type: 'unsubscribe', url: string }
| { type: 'videoExists', id: string }
| { type: 'getChannel', url: string }
*/
function handleMessage(request, sender, sendResponse) {
console.log('message background.js listener got message', request);
// this function must return the value `true` in chrome to signal the response will be async;
// it cannot return a promise
// so in order to use async/await, we need a wrapper
(async () => {
switch (request.type) {
case 'verify': {
return await verifyConnection();
}
case 'cookieState': {
return await getCookieState();
}
case 'sendCookie': {
return await sendCookies();
}
case 'download': {
return await download(request.url);
}
case 'subscribe': {
return await subscribe(request.url, true);
}
case 'unsubscribe': {
let channelId = await getChannelId(request.url);
return await subscribe(channelId, false);
}
case 'videoExists': {
return await videoExists(request.videoId);
}
case 'getChannel': {
return await getChannel(request.channelHandle);
}
default: {
let err = new Error(`unknown message type ${JSON.stringify(request.type)}`);
console.log(err);
throw err;
}
}
})()
.then(value => sendResponse({ success: true, value }))
.catch(e => {
console.error(e);
let message = e?.message ?? e;
if (message === 'Failed to fetch') {
// chrome's error message for failed `fetch` is not very user-friendly
message = 'Could not connect to server';
}
sendResponse({ success: false, value: message });
});
return true;
}
browserType.runtime.onMessage.addListener(handleMessage);