Ctrl K
ComunidadGuías…

chrome-extension/background.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
importScripts('i18n.js', 'provider-adapters.js');
const currentLanguage=async()=>NASDropI18n.resolve((await settings()).language,chrome.i18n.getUILanguage());
const translatedError=async error=>NASDropI18n.error(await currentLanguage(),error.message);
const storageReady = chrome.storage.local.setAccessLevel({accessLevel: 'TRUSTED_CONTEXTS'});
const pageTransfers = new Map();
const SENDNOW_ARM_MS = 60000;
const MENU_LINK = "nasdrop-send-link";
const MENU_PAGE = "nasdrop-send-page";
const ACTIVE_STATUSES = new Set([
  "inspecting", "queued", "ready", "downloading", "waiting_processing", "verifying",
  "extracting", "publishing", "stopping",
]);
const JOB_ALARM = 'nasdrop-job-monitor';
let jobsFlight = null, jobsCache = null, jobsCachedAt = 0, monitorFailures = 0;
const popupPorts = new Set();

async function getJobs(force = false) {
  if (jobsFlight) return jobsFlight;
  if (!force && jobsCache && Date.now() - jobsCachedAt < 2000) return jobsCache;
  jobsFlight = (async () => {
    try {
      const result = await api('/api/jobs', {signal:AbortSignal.timeout(15000)});
      if (!Array.isArray(result.jobs)) throw new Error('Invalid job list received.');
      jobsCache = result; jobsCachedAt = Date.now(); monitorFailures = 0;
      await updateBadge(result.jobs);
      const previous = (await chrome.storage.session.get({passwordJobs:[]})).passwordJobs;
      const waiting = result.jobs.filter(job => job.status === 'password_required');
      const announced = [];
      for (const job of waiting) {
        if (!previous.includes(job.id)) {
          try {
            const language=await currentLanguage();
            await chrome.notifications.create(`nasdrop-password-${job.id}`, {
              type:'basic', iconUrl:'icons/nasdrop-256.png', title:NASDropI18n.t(language,'passwordTitle'),
              message:`${job.name}\n${NASDropI18n.t(language,'passwordMessage')}`,
            });
          } catch { continue; }
        }
        announced.push(job.id);
      }
      await chrome.storage.session.set({passwordJobs:announced});
      if (result.jobs.some(job => ACTIVE_STATUSES.has(job.status))) await chrome.alarms.create(JOB_ALARM,{delayInMinutes:1});
      else await chrome.alarms.clear(JOB_ALARM);
      return result;
    } catch (error) {
      if (error.status === 401) { jobsCache = null; await chrome.alarms.clear(JOB_ALARM); }
      else await chrome.alarms.create(JOB_ALARM,{delayInMinutes:Math.min(5,2 ** monitorFailures++)});
      throw error;
    } finally { jobsFlight = null; }
  })();
  return jobsFlight;
}

chrome.runtime.onConnect.addListener(port => {
  if (port.name !== 'nasdrop-popup' || port.sender?.url !== chrome.runtime.getURL('popup.html')) return;
  popupPorts.add(port);
  port.onDisconnect.addListener(() => popupPorts.delete(port));
});
chrome.alarms.onAlarm.addListener(alarm => {
  if (alarm.name !== JOB_ALARM) return;
  if (popupPorts.size) { chrome.alarms.create(JOB_ALARM,{delayInMinutes:1}); return; }
  getJobs().catch(() => {});
});

function normalizeBaseUrl(value) {
  const url = new URL(String(value || "").trim());
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('NASDrop address must use HTTP or HTTPS.');
  if (url.username || url.password) throw new Error('Enter a server address without embedded credentials.');
  url.hash = '';
  url.search = '';
  return url.href.replace(/\/$/, '');
}

async function settings() {
  await storageReady;
  return chrome.storage.local.get({ baseUrl: '', token: '', autoExtract: true, username: '', language:'auto' });
}

async function api(path, init = {}, requireAuth = true) {
  const saved = await settings();
  if (!saved.baseUrl) throw new Error('Open the NASDrop extension and enter your server address.');
  if (requireAuth && !saved.token) throw Object.assign(new Error('Sign in to NASDrop first.'), { status: 401 });
  const headers = { 'content-type': 'application/json', ...(init.headers || {}) };
  if (requireAuth) headers.authorization = `Bearer ${saved.token}`;
  let response;
  try {
    response = await fetch(`${normalizeBaseUrl(saved.baseUrl)}${path}`, { ...init, headers, redirect:'error', credentials:'omit' });
  } catch (_) {
    throw new Error('Could not reach the NASDrop server. Check its address and network connection.');
  }
  const payload = await response.json().catch(() => ({}));
  if (!response.ok) {
    if (response.status === 401 && requireAuth) await chrome.storage.local.remove('token');
    const error = new Error(payload.error || `NASDrop request failed (${response.status}).`);
    error.status = response.status;
    error.code = payload.code || '';
    throw error;
  }
  return payload;
}

async function updateBadge(jobs) {
  const count = (jobs || []).filter(job => ACTIVE_STATUSES.has(job.status)).length;
  await chrome.action.setBadgeBackgroundColor({ color: '#2563eb' });
  await chrome.action.setBadgeText({ text: count ? String(Math.min(count, 99)) : '' });
}

async function getState() {
  const saved = await settings();
  if (!saved.baseUrl || !saved.token) return { ...saved, connected: false, jobs: [] };
  try {
    const [status, jobResult] = await Promise.all([api('/api/status'), getJobs()]);
    await updateBadge(jobResult.jobs);
    return { ...saved, connected: true, status, jobs: jobResult.jobs };
  } catch (error) {
    return { ...saved, token: error.status === 401 ? '' : saved.token, connected: false, jobs: [], passwordChangeRequired:error.code === 'password_change_required', error: error.message };
  }
}

async function login({ baseUrl, username, password }) {
  const normalized = normalizeBaseUrl(baseUrl);
  const previous = await settings();
  if (previous.baseUrl !== normalized) await chrome.storage.local.remove(['token', 'username']);
  jobsCache = null;
  await chrome.storage.session.remove('passwordJobs');
  await chrome.storage.session.remove('sendNowArm');
  await chrome.storage.local.set({ baseUrl: normalized });
  const result = await api('/api/login', {
    method: 'POST',
    body: JSON.stringify({ username: String(username || '').trim(), password: String(password || '') }),
  }, false);
  await chrome.storage.local.set({ token: result.token, username: result.username || String(username || '').trim() });
  if (result.password_change_required) {
    return { ...(await settings()), connected:false, jobs:[], passwordChangeRequired:true };
  }
  return getState();
}

async function logout() {
  try { await api('/api/logout', { method: 'POST', body: '{}' }); } catch (_) {}
  await chrome.storage.local.remove(['token', 'username']);
  jobsCache = null;
  await chrome.alarms.clear(JOB_ALARM);
  await chrome.storage.session.remove('passwordJobs');
  await chrome.storage.session.remove('sendNowArm');
  await updateBadge([]);
  return { ok: true };
}

async function submitUrl(rawUrl, notify = false, handoffPage = '', options = {}) {
  let source;
  try {
    source = new URL(String(rawUrl || '').trim());
    if (!['http:', 'https:'].includes(source.protocol)) throw new Error();
  } catch (_) {
    throw new Error('Enter a valid HTTP or HTTPS download link.');
  }
  const saved = await settings();
  const sourceProvider = NASDropProviders.provider(source.href);
  const downloadKey = String(options.downloadKey || '');
  if (downloadKey && sourceProvider !== 'gigafile') throw new Error('A GigaFile download key can only be used with a GigaFile link.');
  if (downloadKey.length > 4) throw new Error('GigaFile download key must be 1–4 characters.');
  const common = {target:'',extract:saved.autoExtract !== false,password:saved.autoExtract === false ? '' : String(options.password || '')};
  let asyncEnqueueSupported = sourceProvider === 'gigafile';
  if (sourceProvider === 'gofile') {
    if (typeof options.asyncEnqueueSupported === 'boolean') asyncEnqueueSupported=options.asyncEnqueueSupported;
    else {
      const status=await api('/api/status');
      asyncEnqueueSupported=Array.isArray(status.async_enqueue_providers) && status.async_enqueue_providers.includes('gofile');
    }
  }
  if (asyncEnqueueSupported) {
    const body={url:source.href,...common};
    if (downloadKey) body.download_key=downloadKey;
    const started=await api('/api/enqueue',{method:'POST',body:JSON.stringify(body)}).catch(error=>{
      if (!error.status) throw new Error('Result unknown. Check NASDrop jobs before retrying.');
      throw error;
    });
    let fallbackName=source.pathname.split('/').filter(Boolean).at(-1) || `${sourceProvider === 'gofile' ? 'GoFile' : 'GigaFile'} download`;
    try { fallbackName=decodeURIComponent(fallbackName); } catch (_) { /* Keep the encoded safe fallback. */ }
    const file={name:started.job?.name || fallbackName};
    if (notify) await chrome.notifications.create({type:'basic',iconUrl:'icons/nasdrop-256.png',title:'NASDrop',message:started.count > 1 ? NASDropI18n.t(await currentLanguage(),'addedMany',{count:started.count}) : file.name}).catch(()=>{});
    try { await getJobs(true); } catch (_) { /* The accepted job remains successful. */ }
    return {file,...started};
  }
  const handoffProvider = handoffPage ? NASDropProviders.provider(handoffPage) : '';
  const inspectBody = ['akirabox','vikingfile','sendnow','xshare'].includes(handoffProvider)
    ? {url:NASDropProviders.share(handoffPage), resolved_url:source.href, provider:handoffProvider}
    : {url:source.href};
  const inspected = await api('/api/inspect', {
    method: 'POST',
    body: JSON.stringify(inspectBody),
  });
  const startBody = { ...inspected.file, ...common };
  const started = await api('/api/start', {
    method: 'POST',
    body: JSON.stringify(startBody),
  }).catch(error => {
    if (!error.status) throw new Error('Result unknown. Check NASDrop jobs before retrying.');
    throw error;
  });
  if (notify) {
    const title = NASDropI18n.t(await currentLanguage(),'addedMany',{count:started.count});
    await chrome.notifications.create({
      type: 'basic',
      iconUrl: 'icons/nasdrop-256.png',
      title: 'NASDrop',
      message: started.count > 1 ? title : inspected.file.name,
    }).catch(() => {});
  }
  try {
    const jobResult = await getJobs(true);
    await updateBadge(jobResult.jobs);
  } catch (_) { /* An accepted job remains successful even if refresh fails. */ }
  return { file: inspected.file, ...started };
}

async function notifyError(error) {
  await chrome.notifications.create({
    type: 'basic',
    iconUrl: 'icons/nasdrop-256.png',
    title: 'NASDrop',
    message: await translatedError(error),
  });
}

async function updateMenus() {
  const language=await currentLanguage();
  await new Promise(resolve=>chrome.contextMenus.removeAll(resolve));
  chrome.contextMenus.create({id:MENU_LINK,title:NASDropI18n.t(language,'menuLink'),contexts:['link']});
  chrome.contextMenus.create({id:MENU_PAGE,title:NASDropI18n.t(language,'menuPage'),contexts:['page']});
}
chrome.runtime.onInstalled.addListener(()=>{updateMenus().catch(()=>{});});
chrome.runtime.onStartup?.addListener(()=>{updateMenus().catch(()=>{});});

chrome.contextMenus.onClicked.addListener((info, tab) => {
  const url = info.menuItemId === MENU_LINK ? info.linkUrl : (info.pageUrl || tab?.url);
  submitUrl(url, true).catch(notifyError);
});

async function activeSendNowArm() {
  const arm = (await chrome.storage.session.get({sendNowArm:null})).sendNowArm;
  if (!arm || arm.expiresAt <= Date.now()) {
    if (arm) await chrome.storage.session.remove('sendNowArm');
    return null;
  }
  return arm;
}

async function sendNowResult(tabId, result) {
  await chrome.tabs.sendMessage(tabId,{type:'sendNowResult',...result}).catch(() => {});
}

async function captureSendNowDownload(item) {
  const arm = await activeSendNowArm();
  if (!arm) return;
  let referrer;
  try { referrer = new URL(item.referrer || ''); } catch { return; }
  if (referrer.protocol !== 'https:' || !['send.now','www.send.now'].includes(referrer.hostname)
    || referrer.pathname !== '/' || referrer.search || referrer.hash) return;
  const url = item.finalUrl || item.url || '';
  if (!NASDropProviders.allowedSubmission(arm.source, url)) return;
  await chrome.storage.session.remove('sendNowArm');
  await chrome.downloads.cancel(item.id).catch(() => {});
  await chrome.downloads.erase({id:item.id}).catch(() => {});
  try {
    await submitFromPage(arm.source, url);
    await sendNowResult(arm.tabId,{ok:true});
  } catch (error) {
    await sendNowResult(arm.tabId,{ok:false,error:await translatedError(error)});
  }
}
chrome.downloads.onCreated.addListener(item => { void captureSendNowDownload(item); });

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (sender.id !== chrome.runtime.id) return false;
  // Provider content scripts never receive settings, credentials or job metadata.
  if (sender.tab) {
    const page = sender.url || '';
    if (sender.frameId !== 0) return false;
    let operation;
    if (message?.type === 'pageArmDownload') {
      let current, source;
      try { current=new URL(page); source=NASDropProviders.share(message.source); } catch { return false; }
      if (!source || NASDropProviders.provider(source) !== 'sendnow'
        || current.protocol !== 'https:' || !['send.now','www.send.now'].includes(current.hostname)
        || current.pathname !== '/' || current.search || current.hash || current.origin !== new URL(source).origin) return false;
      operation = pageReadiness(source).then(async readiness => {
        if (!readiness.ready) throw Object.assign(new Error('Sign in to NASDrop first.'), {code:'login'});
        if (readiness.handoffSupported === false) throw Object.assign(new Error('This NASDrop server does not support browser handoff for this site yet.'), {code:'serverUnsupported'});
        const active = await activeSendNowArm();
        if (active && active.tabId !== sender.tab.id) throw new Error('Another Send.now handoff is already waiting for its download.');
        await chrome.storage.session.set({sendNowArm:{tabId:sender.tab.id,source,expiresAt:Date.now()+SENDNOW_ARM_MS}});
        return {armed:true,language:readiness.language};
      });
    } else if (message?.type === 'pageArmXShare') {
      const source=NASDropProviders.share(message.source);
      if (!source || source !== NASDropProviders.share(page) || NASDropProviders.provider(source) !== 'xshare') return false;
      operation = pageReadiness(source).then(readiness => {
        if (!readiness.ready) throw Object.assign(new Error('Sign in to NASDrop first.'), {code:'login'});
        if (readiness.handoffSupported === false) throw Object.assign(new Error('This NASDrop server does not support browser handoff for this site yet.'), {code:'serverUnsupported'});
        return {armed:true,language:readiness.language};
      });
    } else if (!NASDropProviders.share(page)) return false;
    else if (message?.type === 'pageReady') {
      operation = pageReadiness(page);
    } else if (message?.type === 'pageSubmit' && NASDropProviders.allowedSubmission(page, message.url)) {
      const key = `${sender.tab.id}:${message.url}`;
      if (pageTransfers.has(key)) operation = pageTransfers.get(key);
      else {
        operation = submitFromPage(page, message.url, {downloadKey:message.downloadKey}).then(result => ({count: result.count}));
        pageTransfers.set(key, operation);
        operation.finally(() => pageTransfers.delete(key)).catch(() => {});
      }
    } else return false;
    operation.then(result => sendResponse({ok:true, result})).catch(async error => sendResponse({ok:false, error:await translatedError(error), code:error.code}));
    return true;
  }
  if (sender.url !== chrome.runtime.getURL('popup.html')) return false;
  const actions = {
    getState,
    getJobs: () => getJobs(Boolean(message.force)),
    clearCompleted: async () => {
      const result = await api('/api/jobs/completed/clear',{method:'POST',body:'{}'});
      jobsCache = null;
      return result;
    },
    jobPassword: () => updateJob(message, 'password'),
    jobDownloadKey: () => updateJob(message, 'download-key'),
    jobPause: () => controlJob(message, 'pause'),
    jobResume: () => controlJob(message, 'resume'),
    jobDelete: () => controlJob(message, 'delete'),
    jobProcessing: () => updateJob(message, 'processing'),
    login: () => login(message),
    logout,
    submit: () => submitFromPopup(message),
    savePreferences: async () => {
      const values={};
      if(typeof message.autoExtract==='boolean')values.autoExtract=message.autoExtract;
      if(message.language!==undefined) {
        if(!['auto',...NASDropI18n.locales].includes(message.language))throw new Error('Invalid language.');
        values.language=message.language;
      }
      await chrome.storage.local.set(values);
      if(values.language!==undefined)await updateMenus();
      return { ok: true };
    },
  };
  const action = actions[message?.type];
  if (!action) return false;
  action().then(result => sendResponse({ ok: true, result })).catch(async error => {
    sendResponse({ ok: false, error: await translatedError(error), status: error.status || 0 });
  });
  return true;
});

async function controlJob(message, action) {
  if (!/^[a-f0-9]{12}$/.test(message.id || '')) throw new Error('Invalid job ID.');
  const path=action==='delete' ? '/api/jobs/delete' : `/api/jobs/${message.id}/${action}`;
  const result=await api(path,{method:'POST',body:JSON.stringify(action==='delete'?{ids:[message.id]}:{})});
  jobsCache=null;
  await chrome.alarms.create(JOB_ALARM,{delayInMinutes:1});
  return result;
}

async function updateJob(message, action) {
  if (!/^[a-f0-9]{12}$/.test(message.id || '')) throw new Error('Invalid job ID.');
  let body;
  if (action === 'password') body = {password:String(message.password || '')};
  else if (action === 'download-key') {
    const downloadKey=String(message.downloadKey || '');
    if (downloadKey.length < 1 || downloadKey.length > 4) throw new Error('GigaFile download key must be 1–4 characters.');
    body={download_key:downloadKey};
  } else body = {extract:message.extract};
  if (action === 'processing') {
    if (typeof body.extract !== 'boolean') throw new Error('Invalid extraction option.');
    if (message.password) body.password = String(message.password);
  }
  const result = await api(`/api/jobs/${message.id}/${action}`,{method:'POST',body:JSON.stringify(body)});
  jobsCache = null;
  await chrome.alarms.create(JOB_ALARM,{delayInMinutes:1});
  return result;
}

async function pageReadiness(page) {
  const saved = await settings();
  const ready = Boolean(saved.baseUrl && saved.token);
  const language=NASDropI18n.resolve(saved.language,chrome.i18n.getUILanguage());
  const provider = NASDropProviders.provider(page);
  if (!ready || !['akirabox','vikingfile','sendnow','xshare','gigafile','gofile'].includes(provider)) return {ready,language};
  const status = await api('/api/status');
  if (provider === 'gigafile') return {ready:true,language,gigafileDownloadKeySupported:status.gigafile_download_key === true};
  if (provider === 'gofile') return {ready:true,language,asyncEnqueueSupported:Array.isArray(status.async_enqueue_providers) && status.async_enqueue_providers.includes('gofile')};
  return {ready:true, language,handoffSupported:Array.isArray(status.browser_handoff_providers) && status.browser_handoff_providers.includes(provider)};
}

async function submitFromPopup(message) {
  if (message.downloadKey) {
    const status=await api('/api/status');
    if (status.gigafile_download_key !== true) {
      const error=new Error('This NASDrop server does not support GigaFile download keys yet.');
      error.code='serverUnsupported'; throw error;
    }
  }
  return submitUrl(message.url,false,'',message);
}

async function submitFromPage(page, url, options = {}) {
  const readiness = await pageReadiness(page);
  if (readiness.handoffSupported === false) {
    const error = new Error('This NASDrop server does not support browser handoff for this site yet.');
    error.code = 'serverUnsupported'; throw error;
  }
  if (NASDropProviders.provider(page) === 'gigafile' && options.downloadKey && !readiness.gigafileDownloadKeySupported) {
    const error = new Error('This NASDrop server does not support GigaFile download keys yet.');
    error.code = 'serverUnsupported'; throw error;
  }
  return submitUrl(url, false, page, {...options,asyncEnqueueSupported:readiness.asyncEnqueueSupported});
}