Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
248fbd5f0f | |||
20e011bb55 | |||
bbb4a910a0 | |||
1cbde09ac6 | |||
d107b1a49f | |||
505bab6d34 | |||
e1459856c2 |
2110
3CX_TAPI.user.js
2110
3CX_TAPI.user.js
File diff suppressed because it is too large
Load Diff
@ -9,13 +9,11 @@ module.exports = {
|
||||
source: pkg.repository.url,
|
||||
downloadURL: 'https://source.cp-austria.at/git/CPATRD/3cx_tapi/raw/branch/master/3CX_TAPI.user.js',
|
||||
match: [
|
||||
'https://192.168.0.154:5001/webclient*',
|
||||
'https://cpsolution.my3cx.at:5001/webclient*'
|
||||
'https://192.168.0.154:5001/*',
|
||||
'https://cpsolution.my3cx.at:5001/*'
|
||||
],
|
||||
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',
|
||||
|
@ -15,8 +15,6 @@ const webpackConfig = {
|
||||
},
|
||||
externals: {
|
||||
jquery: '$',
|
||||
axios: 'axios',
|
||||
'axios-userscript-adapter': 'axiosGmxhrAdapter'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
5304
package-lock.json
generated
5304
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "3cx-tapi",
|
||||
"description": "3CX CP Tapi and Projectmanager integration",
|
||||
"version": "9.1.0",
|
||||
"version": "9.2.1",
|
||||
"author": {
|
||||
"name": "Daniel Triendl",
|
||||
"email": "d.triendl@cp-solutions.at"
|
||||
@ -24,33 +24,31 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"axios-userscript-adapter": "^0.2.0-alpha.2",
|
||||
"chrono-node": "^2.6.3"
|
||||
"chrono-node": "^2.7.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"babel-loader": "^9.1.2",
|
||||
"@types/greasemonkey": "^4.0.7",
|
||||
"@babel/core": "^7.25.8",
|
||||
"@babel/preset-env": "^7.25.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.8.1",
|
||||
"@typescript-eslint/parser": "^8.8.1",
|
||||
"babel-loader": "^9.2.1",
|
||||
"browserslist": "^4.21.9",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"less": "4.1.3",
|
||||
"less-loader": "^11.1.3",
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-loader": "^9.4.3",
|
||||
"typescript": "^5.1.3",
|
||||
"eslint-plugin-promise": "^7.1.0S",
|
||||
"less": "4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.6.3",
|
||||
"userscript-metadata-webpack-plugin": "^0.4.0",
|
||||
"webpack": "^5.88.0",
|
||||
"webpack-bundle-analyzer": "^4.9.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-livereload-plugin": "3.0.2",
|
||||
"webpack-merge": "^5.9.0"
|
||||
"webpack-merge": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as chrono from 'chrono-node'
|
||||
import { TapiContact } from './tapi-contact'
|
||||
import { axios, extractNumber } from './utils'
|
||||
import { extractNumber } from './utils'
|
||||
import GM_fetch from './gm-fetch'
|
||||
|
||||
export class CallHistory {
|
||||
private callerIds: { [number: string]: TapiContact } = {}
|
||||
@ -85,10 +86,10 @@ export class CallHistory {
|
||||
if (this.callerIds[number] !== undefined) {
|
||||
this.updateCallHistoryEntry(element, this.callerIds[number])
|
||||
} else {
|
||||
var response = await axios.get<TapiContact>('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number))
|
||||
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number))
|
||||
var callerId: TapiContact = { tD_NAME: '' }
|
||||
if (response.status === 200) {
|
||||
callerId = response.data
|
||||
callerId = await response.json() as TapiContact
|
||||
}
|
||||
console.log('TAPI call histroy callerid response', number, response, callerId)
|
||||
this.callerIds[number] = callerId
|
||||
|
@ -1,5 +1,6 @@
|
||||
import GM_fetch from './gm-fetch'
|
||||
import { TapiContact } from './tapi-contact'
|
||||
import { axios, extractNumber } from './utils'
|
||||
import { extractNumber } from './utils'
|
||||
|
||||
export class CallNotification {
|
||||
public async showCallNotification (element: HTMLElement) {
|
||||
@ -13,19 +14,19 @@ export class CallNotification {
|
||||
}
|
||||
|
||||
console.log('TAPI searching callerid for', number)
|
||||
var response = await axios.get<TapiContact>('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number))
|
||||
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/callerid/' + encodeURIComponent(number))
|
||||
console.log('TAPI callerid response', response)
|
||||
var notification = {
|
||||
text: number
|
||||
}
|
||||
if (response.status === 200) {
|
||||
var callerId = response.data
|
||||
var callerId = await response.json() as TapiContact
|
||||
if (callerId) {
|
||||
notification.text = callerId.tD_NAME + '\r\n' + number + ' (' + callerId.tD_MEDIUM + ')'
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
GM.notification(notification)
|
||||
GM.notification(notification.text, 'TAPI Anruf')
|
||||
}
|
||||
}
|
||||
|
3
src/decs.d.ts
vendored
3
src/decs.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare module 'axios-userscript-adapter'
|
||||
|
||||
declare const GM: any
|
55
src/gm-fetch/index.ts
Normal file
55
src/gm-fetch/index.ts
Normal 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
145
src/gm-fetch/utils.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
}
|
||||
*/
|
@ -33,6 +33,9 @@
|
||||
border-bottom: 1px solid #d4d4d4;
|
||||
color: #000;
|
||||
}
|
||||
.tapi-search-autocomplete-items div p {
|
||||
margin: 0;
|
||||
}
|
||||
.tapi-search-autocomplete-items div:hover, .tapi-search-autocomplete-active {
|
||||
/*when hovering an item:*/
|
||||
background-color: #E7E6E6 !important;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import './search.css'
|
||||
import { TapiContact } from './tapi-contact'
|
||||
import { debounce } from './debounce'
|
||||
import { axios, fireChangeEvents } from './utils'
|
||||
import { fireChangeEvents } from './utils'
|
||||
import GM_fetch from './gm-fetch'
|
||||
|
||||
export class Search {
|
||||
private currentSearchText = ''
|
||||
@ -102,9 +103,9 @@ export class Search {
|
||||
return
|
||||
}
|
||||
console.log('Searching TAPI')
|
||||
var response = await axios.get<TapiContact[]>('http://cpatapi.cpsrvweb2016.cp-austria.at/search?query=' + encodeURIComponent(searchText))
|
||||
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/search?query=' + encodeURIComponent(searchText))
|
||||
console.log('TAPI Search response', response)
|
||||
var contacts = response.data
|
||||
var contacts = await response.json() as TapiContact[]
|
||||
console.log('TAPI Contacts', contacts)
|
||||
this.removeSearchResults()
|
||||
this.currentSearchText = searchText
|
||||
@ -117,8 +118,9 @@ export class Search {
|
||||
contacts.forEach(contact => {
|
||||
var item = document.createElement('div');
|
||||
item.setAttribute('class', 'tapi-search-autocomplete-item')
|
||||
item.appendChild(document.createTextNode(contact.tD_NAME))
|
||||
item.appendChild(document.createTextNode(contact.tD_MEDIUM + ': ' + contact.tD_NUMBER_TAPI))
|
||||
var p = document.createElement('p')
|
||||
p.innerHTML = contact.tD_NAME + '<br>' + contact.tD_MEDIUM + ': ' + contact.tD_NUMBER_TAPI
|
||||
item.appendChild(p)
|
||||
item.onclick = () => { this.dial(contact.tD_NUMBER_TAPI) }
|
||||
item.onmouseover = () => { this.selectResult(item) }
|
||||
item.dataset.tapiNumber = contact.tD_NUMBER_TAPI
|
||||
|
@ -1,5 +1,5 @@
|
||||
import GM_fetch from './gm-fetch';
|
||||
import './status.css';
|
||||
import { axios } from './utils';
|
||||
import { ZcStatus } from './zc-status';
|
||||
|
||||
declare function waitForKeyElements(selectorOrFunction: any, callback: any, waitOnce: boolean): any;
|
||||
@ -26,16 +26,18 @@ export class Status {
|
||||
private async checkStatus() {
|
||||
if (this._enabled) {
|
||||
try {
|
||||
var response = await axios.get<ZcStatus>('http://cpatapi.cpsrvweb2016.cp-austria.at/availability/' + encodeURIComponent(this._user));
|
||||
|
||||
var response = await GM_fetch('http://cpatapi.cpsrvweb2016.cp-austria.at/availability/' + encodeURIComponent(this._user));
|
||||
if (response.status == 200) {
|
||||
var status = response.data;
|
||||
var status = await response.json() as ZcStatus;
|
||||
if (this._currentStatus !== status.loggedIn) {
|
||||
this._currentStatus = status.loggedIn;
|
||||
console.log('New status, loggedIn', this._currentStatus);
|
||||
(document.getElementsByTagName("wcavatar")[0] as HTMLAnchorElement).click();
|
||||
var accMenu = document.getElementsByTagName("wc-account-menu")[0];
|
||||
var avatar = accMenu.getElementsByTagName("app-avatar")[0] as HTMLAnchorElement;
|
||||
avatar.click();
|
||||
setTimeout(() => {
|
||||
var statusId = this._currentStatus ? this._statusOn : this._statusOff;
|
||||
console.log('Clicking status', statusId);
|
||||
(document.getElementById(statusId) as HTMLSpanElement).click();
|
||||
}, 1000);
|
||||
}
|
||||
@ -70,7 +72,7 @@ export class Status {
|
||||
'<div role="document" class="modal-dialog">' +
|
||||
' <div class="modal-content">' +
|
||||
' <div class="modal-header">' +
|
||||
' <h4 class="modal-title float-left">ZeitConsens Status</h4><button id="zc-btnClose" type="button" aria-label="Close" class="close float-right"><span aria-hidden="true">×</span></button>' +
|
||||
' <h4 class="modal-title">ZeitConsens Status</h4><button id="zc-btnClose" type="button" aria-label="Close" class="btn-close" data-qa="modal-cross"></button>' +
|
||||
' </div>' +
|
||||
' <div class="modal-body">' +
|
||||
' <div class="form-group">' +
|
||||
@ -96,13 +98,13 @@ export class Status {
|
||||
' <label class="i-checks" for="tapi-zc-enabled">' +
|
||||
' <input type="checkbox" id="tapi-zc-enabled">' +
|
||||
' <i></i><span>Enabled</span>' +
|
||||
' </label>'
|
||||
' </div>';
|
||||
' </div>' +
|
||||
' <div class="modal-footer">' +
|
||||
' <button id="zc-btnOk" type="button" class="btn btn-primary">OK </button>' +
|
||||
' <button id="zc-btnCancel" type="button" class="btn btn-light">Cancel </button>' +
|
||||
' </label>' +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
//' <div class="modal-footer">' +
|
||||
//' <button id="zc-btnOk" type="button" class="btn btn-primary" data-qa="modal-ok">OK </button>' +
|
||||
//' <button id="zc-btnCancel" type="button" class="btn btn-border" data-qa="modal-close">Cancel </button>' +
|
||||
//' </div>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
var modal = document.createElement('modal-container');
|
||||
@ -110,7 +112,7 @@ export class Status {
|
||||
modal.classList.add('modal');
|
||||
modal.classList.add('fade');
|
||||
modal.innerHTML = html;
|
||||
var body = document.getElementsByTagName('body')[0].appendChild(modal);
|
||||
document.getElementsByTagName('body')[0].appendChild(modal);
|
||||
|
||||
var btnClose = document.getElementById('zc-btnClose');
|
||||
btnClose.onclick = () => {
|
||||
|
20
src/utils.ts
20
src/utils.ts
@ -1,23 +1,3 @@
|
||||
/**
|
||||
* @typedef {Object} AxiosResponse
|
||||
* @property {Object} data
|
||||
* @property {Object} headers
|
||||
* @property {Object} config
|
||||
* @property {Object} request
|
||||
* @property {number} code
|
||||
* @property {string} statusText
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object} AxiosError
|
||||
* @property {AxiosResponse} response
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import adapter from 'axios-userscript-adapter'
|
||||
|
||||
axios.defaults.adapter = adapter
|
||||
|
||||
export { axios }
|
||||
|
||||
export function extractNumber (s: string) {
|
||||
var match = /(\+?[0-9]{4,})/.exec(s)
|
||||
if (!match) {
|
||||
|
@ -2,8 +2,8 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"module": "ESNext",
|
||||
"target": "ES2022",
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
|
Reference in New Issue
Block a user