diff --git a/config/metadata.cjs b/config/metadata.cjs index e8c002f..2eb6b68 100644 --- a/config/metadata.cjs +++ b/config/metadata.cjs @@ -14,8 +14,6 @@ module.exports = { ], require: [ 'https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js', - `https://cdn.jsdelivr.net/npm/axios@${pkg.dependencies.axios}/dist/axios.min.js`, - `https://cdn.jsdelivr.net/npm/axios-userscript-adapter@${pkg.dependencies['axios-userscript-adapter']}/dist/axiosGmxhrAdapter.min.js` ], grant: [ 'GM.xmlHttpRequest', diff --git a/package-lock.json b/package-lock.json index eb59a75..6eed2ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,13 @@ { "name": "3cx-tapi", - "version": "9.1.1", + "version": "9.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "3cx-tapi", - "version": "9.1.1", + "version": "9.2.0", "dependencies": { - "@trim21/gm-fetch": "^0.1.15", "chrono-node": "^2.7.7" }, "devDependencies": { @@ -1864,12 +1863,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@trim21/gm-fetch": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/@trim21/gm-fetch/-/gm-fetch-0.1.15.tgz", - "integrity": "sha512-197WkYDd1XY8eDNWMBWSrAV77kCJzvh8EOnKSgIoHDXTb1AFLDN1iFnK/Y2x4XTOUDdOfUQd25rKUlVGWmQYhQ==", - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", diff --git a/package.json b/package.json index 838e2f2..c140356 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ }, "private": true, "dependencies": { - "chrono-node": "^2.7.7", - "@trim21/gm-fetch": "^0.1.15" + "chrono-node": "^2.7.7" }, "devDependencies": { "@types/greasemonkey": "^4.0.7", diff --git a/src/call-history.ts b/src/call-history.ts index 8f43c11..98e416a 100644 --- a/src/call-history.ts +++ b/src/call-history.ts @@ -1,7 +1,7 @@ import * as chrono from 'chrono-node' import { TapiContact } from './tapi-contact' import { extractNumber } from './utils' -import GM_fetch from '@trim21/gm-fetch' +import GM_fetch from './gm-fetch' export class CallHistory { private callerIds: { [number: string]: TapiContact } = {} diff --git a/src/call-notification.ts b/src/call-notification.ts index c6358e2..3b16348 100644 --- a/src/call-notification.ts +++ b/src/call-notification.ts @@ -1,4 +1,4 @@ -import GM_fetch from '@trim21/gm-fetch' +import GM_fetch from './gm-fetch' import { TapiContact } from './tapi-contact' import { extractNumber } from './utils' diff --git a/src/gm-fetch/index.ts b/src/gm-fetch/index.ts new file mode 100644 index 0000000..27da486 --- /dev/null +++ b/src/gm-fetch/index.ts @@ -0,0 +1,55 @@ +import { parseGMResponse } from "./utils"; + +export default async function GM_fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const request = new Request(input, init); + + let data: string | undefined; + if (init?.body) { + data = await request.text(); + } + + return await XHR(request, init, data); +} + +function XHR(request: Request, init: RequestInit | undefined, data: string | undefined): Promise { + return new Promise((resolve, reject) => { + if (request.signal && request.signal.aborted) { + return reject(new DOMException("Aborted", "AbortError")); + } + + GM.xmlHttpRequest({ + url: request.url, + method: gmXHRMethod(request.method.toUpperCase()), + headers: Object.fromEntries(new Headers(init?.headers).entries()), + data: data, + responseType: "blob", + onload(res) { + resolve(parseGMResponse(request, res)); + }, + onabort() { + reject(new DOMException("Aborted", "AbortError")); + }, + ontimeout() { + reject(new TypeError("Network request failed, timeout")); + }, + onerror(err) { + reject(new TypeError("Failed to fetch: " + err.finalUrl)); + }, + }); + }); +} + +const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"] as const; + +// a ts type helper to narrow type +function includes(array: ReadonlyArray, element: U): element is T { + return array.includes(element as T); +} + +function gmXHRMethod(method: string): (typeof httpMethods)[number] { + if (includes(httpMethods, method)) { + return method; + } + + throw new Error(`unsupported http method ${method}`); +} diff --git a/src/gm-fetch/utils.ts b/src/gm-fetch/utils.ts new file mode 100644 index 0000000..7e3d25f --- /dev/null +++ b/src/gm-fetch/utils.ts @@ -0,0 +1,145 @@ +export function parseRawHeaders(h: string): Headers { + const s = h.trim(); + if (!s) { + return new Headers(); + } + + const array: [string, string][] = s.split("\r\n").map((value) => { + let s = value.split(":"); + return [s[0].trim(), s[1].trim()]; + }); + + return new Headers(array); +} + +export function parseGMResponse(req: Request, res: GM.Response): Response { + return new ResImpl(res.response as Blob, { + statusCode: res.status, + statusText: res.statusText, + headers: parseRawHeaders(res.responseHeaders), + finalUrl: res.finalUrl, + redirected: res.finalUrl === req.url, + }); +} + +interface ResInit { + redirected: boolean; + headers: Headers; + statusCode: number; + statusText: string; + finalUrl: string; +} + +class ResImpl implements Response { + private _bodyUsed: boolean; + private readonly rawBody: Blob; + private readonly init: ResInit; + + readonly body: ReadableStream | null; + readonly headers: Headers; + readonly redirected: boolean; + readonly status: number; + readonly statusText: string; + readonly type: ResponseType; + readonly url: string; + + constructor(body: Blob, init: ResInit) { + this.rawBody = body; + this.init = init; + + //this.body = toReadableStream(body); + const { headers, statusCode, statusText, finalUrl, redirected } = init; + this.headers = headers; + this.status = statusCode; + this.statusText = statusText; + this.url = finalUrl; + this.type = "basic"; + this.redirected = redirected; + this._bodyUsed = false; + } + + get bodyUsed(): boolean { + return this._bodyUsed; + } + + get ok(): boolean { + return this.status < 300; + } + + arrayBuffer(): Promise { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read"); + } + this._bodyUsed = true; + return this.rawBody.arrayBuffer(); + } + + blob(): Promise { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read"); + } + this._bodyUsed = true; + // `slice` will use empty string as default value, so need to pass all arguments. + return Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type)); + } + + clone(): Response { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read"); + } + return new ResImpl(this.rawBody, this.init); + } + + formData(): Promise { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read"); + } + this._bodyUsed = true; + return this.rawBody.text().then(decode); + } + + async json(): Promise { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'json' on 'Response': body stream already read"); + } + this._bodyUsed = true; + return JSON.parse(await this.rawBody.text()); + } + + text(): Promise { + if (this.bodyUsed) { + throw new TypeError("Failed to execute 'text' on 'Response': body stream already read"); + } + this._bodyUsed = true; + return this.rawBody.text(); + } +} + +function decode(body: string) { + const form = new FormData(); + + body + .trim() + .split("&") + .forEach(function (bytes) { + if (bytes) { + const split = bytes.split("="); + const name = split.shift()?.replace(/\+/g, " "); + const value = split.join("=").replace(/\+/g, " "); + form.append(decodeURIComponent(name!), decodeURIComponent(value)); + } + }); + + return form; +} + +/* +function toReadableStream(value: Blob) { + return new ReadableStream({ + start(controller) { + controller.enqueue(value); + controller.close(); + }, + }); +} +*/ diff --git a/src/search.ts b/src/search.ts index f056826..a399be2 100644 --- a/src/search.ts +++ b/src/search.ts @@ -2,7 +2,7 @@ import './search.css' import { TapiContact } from './tapi-contact' import { debounce } from './debounce' import { fireChangeEvents } from './utils' -import GM_fetch from '@trim21/gm-fetch' +import GM_fetch from './gm-fetch' export class Search { private currentSearchText = '' diff --git a/src/status.ts b/src/status.ts index 02c5812..457c43d 100644 --- a/src/status.ts +++ b/src/status.ts @@ -1,6 +1,6 @@ +import GM_fetch from './gm-fetch'; import './status.css'; import { ZcStatus } from './zc-status'; -import GM_fetch from "@trim21/gm-fetch"; declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;