diff --git a/src/core/WakaTimeCore.ts b/src/core/WakaTimeCore.ts index 7284271..a4422d3 100644 --- a/src/core/WakaTimeCore.ts +++ b/src/core/WakaTimeCore.ts @@ -8,7 +8,7 @@ import config from '../config/config'; import { SendHeartbeat } from '../types/heartbeats'; import { GrandTotal, SummariesPayload } from '../types/summaries'; import { ApiKeyPayload, AxiosUserResponse, User } from '../types/user'; -import { generateProjectFromDevSites, IS_FIREFOX } from '../utils'; +import { IS_FIREFOX, generateProjectFromDevSites } from '../utils'; import { getApiKey } from '../utils/apiKey'; import changeExtensionState from '../utils/changeExtensionState'; import contains from '../utils/contains'; @@ -112,7 +112,7 @@ class WakaTimeCore { * Depending on various factors detects the current active tab URL or domain, * and sends it to WakaTime for logging. */ - async recordHeartbeat(): Promise { + async recordHeartbeat(payload = {}): Promise { const apiKey = await getApiKey(); if (!apiKey) { return changeExtensionState('notLogging'); @@ -129,62 +129,69 @@ class WakaTimeCore { if (items.loggingEnabled === true) { await changeExtensionState('allGood'); - const newState = await browser.idle.queryState(config.detectionIntervalInSeconds); - - if (newState === 'active') { - // Get current tab URL. - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - if (tabs.length == 0) return; - - const currentActiveTab = tabs[0]; - - if (!items.trackSocialMedia) { - if (contains(currentActiveTab.url as string, items.socialMediaSites as string)) { - return changeExtensionState('blacklisted'); - } - } - - // Checks dev websites - const project = generateProjectFromDevSites(currentActiveTab.url as string); - - if (items.loggingStyle == 'blacklist') { - if (!contains(currentActiveTab.url as string, items.blacklist as string)) { - await this.sendHeartbeat( - { - hostname: items.hostname as string, - project, - url: currentActiveTab.url as string, - }, - apiKey, - ); - } else { - await changeExtensionState('blacklisted'); - console.log(`${currentActiveTab.url} is on a blacklist.`); - } - } - - if (items.loggingStyle == 'whitelist') { - const heartbeat = this.getHeartbeat( - currentActiveTab.url as string, - items.whitelist as string, - ); - if (heartbeat.url) { - await this.sendHeartbeat( - { - ...heartbeat, - hostname: items.hostname as string, - project: heartbeat.project ?? project, - }, - apiKey, - ); - } else { - await changeExtensionState('whitelisted'); - console.log(`${currentActiveTab.url} is not on a whitelist.`); - } + let newState = ''; + // Detects we are running this code in the extension scope + if (browser.idle as browser.Idle.Static | undefined) { + newState = await browser.idle.queryState(config.detectionIntervalInSeconds); + if (newState !== 'active') { + return changeExtensionState('notLogging'); + } + } + + // Get current tab URL. + let url = ''; + if (browser.tabs as browser.Tabs.Static | undefined) { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (tabs.length == 0) return; + const currentActiveTab = tabs[0]; + url = currentActiveTab.url as string; + } else { + url = document.URL; + } + + if (!items.trackSocialMedia) { + if (contains(url, items.socialMediaSites as string)) { + return changeExtensionState('blacklisted'); + } + } + + // Checks dev websites + const project = generateProjectFromDevSites(url); + + if (items.loggingStyle == 'blacklist') { + if (!contains(url, items.blacklist as string)) { + await this.sendHeartbeat( + { + hostname: items.hostname as string, + project, + url, + }, + apiKey, + payload, + ); + } else { + await changeExtensionState('blacklisted'); + console.log(`${url} is on a blacklist.`); + } + } + + if (items.loggingStyle == 'whitelist') { + const heartbeat = this.getHeartbeat(url, items.whitelist as string); + if (heartbeat.url) { + await this.sendHeartbeat( + { + ...heartbeat, + hostname: items.hostname as string, + project: heartbeat.project ?? project, + }, + apiKey, + payload, + ); + } else { + await changeExtensionState('whitelisted'); + console.log(`${url} is not on a whitelist.`); } } - } else { - await changeExtensionState('notLogging'); } } @@ -265,7 +272,11 @@ class WakaTimeCore { * @param heartbeat * @param debug */ - async sendHeartbeat(heartbeat: SendHeartbeat, apiKey: string): Promise { + async sendHeartbeat( + heartbeat: SendHeartbeat, + apiKey: string, + navigationPayload: Record, + ): Promise { let payload; const loggingType = await this.getLoggingType(); @@ -274,12 +285,20 @@ class WakaTimeCore { if (loggingType == 'domain') { heartbeat.url = getDomainFromUrl(heartbeat.url); payload = this.preparePayload(heartbeat, 'domain'); - await this.sendPostRequestToApi(payload, apiKey, heartbeat.hostname); + await this.sendPostRequestToApi( + { ...payload, ...navigationPayload }, + apiKey, + heartbeat.hostname, + ); } // Send entity in heartbeat else if (loggingType == 'url') { payload = this.preparePayload(heartbeat, 'url'); - await this.sendPostRequestToApi(payload, apiKey, heartbeat.hostname); + await this.sendPostRequestToApi( + { ...payload, ...navigationPayload }, + apiKey, + heartbeat.hostname, + ); } } diff --git a/src/manifests/chrome.json b/src/manifests/chrome.json index f5b6ed4..f8f8795 100644 --- a/src/manifests/chrome.json +++ b/src/manifests/chrome.json @@ -33,5 +33,5 @@ "page": "options.html" }, "permissions": ["alarms", "tabs", "storage", "idle"], - "version": "3.0.10" + "version": "3.0.11" } diff --git a/src/manifests/firefox.json b/src/manifests/firefox.json index aa12d7e..69cd513 100644 --- a/src/manifests/firefox.json +++ b/src/manifests/firefox.json @@ -46,5 +46,5 @@ "storage", "idle" ], - "version": "3.0.10" + "version": "3.0.11" } diff --git a/src/utils/changeExtensionIcon.ts b/src/utils/changeExtensionIcon.ts index 40c6199..2821908 100644 --- a/src/utils/changeExtensionIcon.ts +++ b/src/utils/changeExtensionIcon.ts @@ -22,9 +22,9 @@ export default async function changeExtensionIcon(color?: ColorIconTypes): Promi } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (IS_FIREFOX) { + if (IS_FIREFOX && browser.browserAction) { await browser.browserAction.setIcon({ path: path }); // Support for FF with manifest V2 - } else { + } else if ((browser.action as browser.Action.Static | undefined) !== undefined) { await browser.action.setIcon({ path: path }); // Support for Chrome with manifest V3 } } diff --git a/src/utils/changeExtensionTooltip.ts b/src/utils/changeExtensionTooltip.ts index 432fef8..e9e3187 100644 --- a/src/utils/changeExtensionTooltip.ts +++ b/src/utils/changeExtensionTooltip.ts @@ -14,9 +14,9 @@ export default async function changeExtensionTooltip(text: string): Promise { - const projectIndicatorCharacters = '@@'; - - const lines = list.split('\n'); - for (let i = 0; i < lines.length; i++) { - // strip (http:// or https://) and trailing (`/` or `@@`) - const cleanLine = lines[i] - .trim() - .replace(/(\/|@@)$/, '') - .replace(/^(?:https?:\/\/)?/i, ''); - if (cleanLine === '') continue; - - const projectIndicatorIndex = cleanLine.lastIndexOf(projectIndicatorCharacters); - const projectIndicatorExists = projectIndicatorIndex > -1; - let projectName = null; - let urlFromLine = cleanLine; - if (projectIndicatorExists) { - const start = projectIndicatorIndex + projectIndicatorCharacters.length; - projectName = cleanLine.substring(start); - urlFromLine = cleanLine - .replace(cleanLine.substring(projectIndicatorIndex), '') - .replace(/\/$/, ''); - } - const schemaHttpExists = url.match(/^http:\/\//i); - const schemaHttpsExists = url.match(/^https:\/\//i); - let schema = ''; - if (schemaHttpExists) { - schema = 'http://'; - } - if (schemaHttpsExists) { - schema = 'https://'; - } - const cleanUrl = url - .trim() - .replace(/(\/|@@)$/, '') - .replace(/^(?:https?:\/\/)?/i, ''); - const startsWithUrl = cleanUrl.toLowerCase().includes(urlFromLine.toLowerCase()); - if (startsWithUrl) { - return { - project: projectName, - url: schema + urlFromLine, - }; - } - - const lineRe = new RegExp(cleanLine.replace('.', '.').replace('*', '.*')); - - // If url matches the current line return true - if (lineRe.test(url)) { - return { - project: null, - url: schema + urlFromLine, - }; - } - } - - return { - project: null, - url: null, - }; -}; - -/** - * Sends AJAX request with payload to the heartbeat API as JSON. - * - * @param payload - * @param method - * @returns {*} - */ -const sendPostRequestToApi = async ( - payload: Record, - apiKey = '', - hostname = '', -): Promise => { - try { - const items = await browser.storage.sync.get({ - apiUrl: config.apiUrl, - heartbeatApiEndPoint: config.heartbeatApiEndPoint, - }); - - const request: RequestInit = { - body: JSON.stringify(payload), - credentials: 'omit', - method: 'POST', - }; - if (hostname) { - request.headers = { - 'X-Machine-Name': hostname, - }; - } - const response = await fetch( - `${items.apiUrl}${items.heartbeatApiEndPoint}?api_key=${apiKey}`, - request, - ); - await response.json(); - } catch (err: unknown) { - console.log('Error', err); - } -}; - -/** - * Creates payload for the heartbeat and returns it as JSON. - * - * @param heartbeat - * @param type - * @param debug - * @returns {*} - * @private - */ -const preparePayload = (heartbeat: SendHeartbeat, type: string): Record => { - let browserName = 'chrome'; - let userAgent; - if (IS_FIREFOX) { - browserName = 'firefox'; - userAgent = navigator.userAgent.match(/Firefox\/\S+/g)![0]; - } else { - userAgent = navigator.userAgent.match(/Chrome\/\S+/g)![0]; - } - const payload: Record = { - entity: heartbeat.url, - time: moment().format('X'), - type: type, - user_agent: `${userAgent} ${browserName}-wakatime/${config.version}`, - }; - - if (heartbeat.project) { - payload.project = heartbeat.project; - } - - return payload; -}; - -/** - * Returns a promise with logging type variable. - * - * @returns {*} - * @private - */ -const getLoggingType = async (): Promise => { - const items = await browser.storage.sync.get({ - loggingType: config.loggingType, - }); - - return items.loggingType; -}; - -/** - * Given the heartbeat and logging type it creates a payload and - * sends an ajax post request to the API. - * - * @param heartbeat - * @param debug - */ -const sendHeartbeat = async ( - heartbeat: SendHeartbeat, - apiKey: string, - navigationPayload: Record, -): Promise => { - let payload; - - const loggingType = await getLoggingType(); - // Get only the domain from the entity. - // And send that in heartbeat - if (loggingType == 'domain') { - heartbeat.url = getDomainFromUrl(heartbeat.url); - payload = preparePayload(heartbeat, 'domain'); - await sendPostRequestToApi({ ...payload, ...navigationPayload }, apiKey, heartbeat.hostname); - } - // Send entity in heartbeat - else if (loggingType == 'url') { - payload = preparePayload(heartbeat, 'url'); - await sendPostRequestToApi({ ...payload, ...navigationPayload }, apiKey, heartbeat.hostname); - } -}; - -/** - * Depending on various factors detects the current active tab URL or domain, - * and sends it to WakaTime for logging. - */ -const recordHeartbeat = async (apiKey: string, payload: Record): Promise => { - const items = await browser.storage.sync.get({ - blacklist: '', - hostname: config.hostname, - loggingEnabled: config.loggingEnabled, - loggingStyle: config.loggingStyle, - socialMediaSites: config.socialMediaSites, - trackSocialMedia: config.trackSocialMedia, - whitelist: '', - }); - if (items.loggingEnabled === true) { - if (!items.trackSocialMedia) { - if (contains(document.URL, items.socialMediaSites as string)) { - return; - } - } - - // Checks dev websites - const project = generateProjectFromDevSites(document.URL); - - if (items.loggingStyle == 'blacklist') { - if (!contains(document.URL, items.blacklist as string)) { - await sendHeartbeat( - { - hostname: items.hostname as string, - project, - url: document.URL, - }, - apiKey, - payload, - ); - } else { - console.log(`${document.URL} is on a blacklist.`); - } - } - - if (items.loggingStyle == 'whitelist') { - const heartbeat = getHeartbeat(document.URL, items.whitelist as string); - if (heartbeat.url) { - await sendHeartbeat( - { - ...heartbeat, - hostname: items.hostname as string, - project: heartbeat.project ?? project, - }, - apiKey, - payload, - ); - } else { - console.log(`${document.URL} is not on a whitelist.`); - } - } - } -}; - interface DesignProject { + category: string; editor: string; language: string; project: string; @@ -264,6 +16,7 @@ const parseCanva = (): DesignProject | undefined => { const projectName = (document.head.querySelector('meta[property="og:title"]') as HTMLMetaElement) .content; return { + category: 'Designing', editor: 'Canva', language: 'Canva Design', project: projectName, @@ -277,6 +30,7 @@ const parseFigma = (): DesignProject | undefined => { const projectName = (document.querySelector('span[data-testid="filename"]') as HTMLElement) .innerText; return { + category: 'Designing', editor: 'Figma', language: 'Figma Design', project: projectName, @@ -293,18 +47,12 @@ const getParser: { }; const init = async () => { - const apiKey = await getApiKey(); - if (!apiKey) return; - const { hostname } = document.location; const projectDetails = getParser[hostname]?.(); if (projectDetails) { - await recordHeartbeat(apiKey, { - category: 'Designing', - ...projectDetails, - }); + await WakaTimeCore.recordHeartbeat(projectDetails); } };