Compare commits

...

2 Commits

Author SHA1 Message Date
248fbd5f0f Bump version to 9.2.0 2024-10-14 11:50:06 +02:00
20e011bb55 gm-fetch für Firefox gefixt 2024-10-14 11:49:10 +02:00
10 changed files with 232 additions and 33 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name 3CX TAPI
// @namespace http://cp-solutions.at
// @version 9.2.0
// @version 9.2.1
// @author Daniel Triendl <d.triendl@cp-solutions.at>
// @copyright Copyright 2021 CP Solutions GmbH
// @source https://source.cp-austria.at/git/CPATRD/3cx_tapi.git
@ -9,8 +9,6 @@
// @match https://192.168.0.154:5001/*
// @match https://cpsolution.my3cx.at:5001/*
// @require https://cdn.jsdelivr.net/gh/CoeJoder/waitForKeyElements.js@v1.2/waitForKeyElements.js
// @require https://cdn.jsdelivr.net/npm/axios@undefined/dist/axios.min.js
// @require https://cdn.jsdelivr.net/npm/axios-userscript-adapter@undefined/dist/axiosGmxhrAdapter.min.js
// @grant GM.xmlHttpRequest
// @grant GM.notification
// @grant GM.getValue
@ -4251,7 +4249,7 @@ function fireChangeEvents(element) {
console.debug('change event dispatched for element: ' + element.id);
}
;// ./node_modules/@trim21/gm-fetch/dist/index.mjs
;// ./src/gm-fetch/utils.ts
function parseRawHeaders(h) {
const s = h.trim();
if (!s) {
@ -4273,10 +4271,20 @@ function parseGMResponse(req, res) {
});
}
class ResImpl {
_bodyUsed;
rawBody;
init;
body;
headers;
redirected;
status;
statusText;
type;
url;
constructor(body, init) {
this.rawBody = body;
this.init = init;
this.body = toReadableStream(body);
//this.body = toReadableStream(body);
const { headers, statusCode, statusText, finalUrl, redirected } = init;
this.headers = headers;
this.status = statusCode;
@ -4350,14 +4358,18 @@ function decode(body) {
});
return form;
}
function toReadableStream(value) {
return new ReadableStream({
start(controller) {
controller.enqueue(value);
controller.close();
},
});
/*
function toReadableStream(value: Blob) {
return new ReadableStream({
start(controller) {
controller.enqueue(value);
controller.close();
},
});
}
*/
;// ./src/gm-fetch/index.ts
async function GM_fetch(input, init) {
const request = new Request(input, init);
@ -4405,9 +4417,6 @@ function gmXHRMethod(method) {
throw new Error(`unsupported http method ${method}`);
}
//# sourceMappingURL=index.mjs.map
;// ./src/call-history.ts

View File

@ -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',

11
package-lock.json generated
View File

@ -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",

View File

@ -1,7 +1,7 @@
{
"name": "3cx-tapi",
"description": "3CX CP Tapi and Projectmanager integration",
"version": "9.2.0",
"version": "9.2.1",
"author": {
"name": "Daniel Triendl",
"email": "d.triendl@cp-solutions.at"
@ -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",

View File

@ -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 } = {}

View File

@ -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'

55
src/gm-fetch/index.ts Normal file
View File

@ -0,0 +1,55 @@
import { parseGMResponse } from "./utils";
export default async function GM_fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
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<Response> {
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<T extends U, U>(array: ReadonlyArray<T>, 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}`);
}

145
src/gm-fetch/utils.ts Normal file
View File

@ -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<any>): 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<Uint8Array> | 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<ArrayBuffer> {
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<Blob> {
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<FormData> {
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<any> {
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<string> {
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();
},
});
}
*/

View File

@ -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 = ''

View File

@ -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;