Compare commits
	
		
			10 Commits
		
	
	
		
			v4.0.0
			...
			fec9885a64
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fec9885a64 | ||
|   | 29fc426161 | ||
|   | 231d24b26a | ||
|   | f3693162ab | ||
|   | c09bdd856b | ||
|   | ecb9097f5f | ||
|   | 7db79afca2 | ||
|   | e40a0810ff | ||
|   | 10c1a9185b | ||
|   | 060889df69 | 
							
								
								
									
										15
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | [*] | ||||||
|  | charset=utf-8 | ||||||
|  | end_of_line=lf | ||||||
|  | trim_trailing_whitespace=true | ||||||
|  | insert_final_newline=true | ||||||
|  | indent_style=space | ||||||
|  | indent_size=4 | ||||||
|  |  | ||||||
|  | [{.babelrc,.stylelintrc,.eslintrc,jest.config,*.bowerrc,*.jsb3,*.jsb2,*.json,*.yaml,*.yml}] | ||||||
|  | indent_style=space | ||||||
|  | indent_size=2 | ||||||
|  |  | ||||||
|  | [{*.js,*.vue,*.ts}] | ||||||
|  | indent_style=space | ||||||
|  | indent_size=2 | ||||||
							
								
								
									
										28
									
								
								.eslintrc.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.eslintrc.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | --- | ||||||
|  | extends: | ||||||
|  |   - standard | ||||||
|  |   - plugin:import/recommended | ||||||
|  |   - plugin:import/typescript # this line does the trick | ||||||
|  |  | ||||||
|  | # or configure manually: | ||||||
|  | plugins: | ||||||
|  |   - import | ||||||
|  |   - promise | ||||||
|  |   - standard | ||||||
|  |   - '@typescript-eslint' | ||||||
|  |  | ||||||
|  | parser: '@typescript-eslint/parser' | ||||||
|  |  | ||||||
|  | parserOptions: | ||||||
|  |   sourceType: module | ||||||
|  |   ecmaVersion: 6 | ||||||
|  |  | ||||||
|  | env: | ||||||
|  |     browser: true | ||||||
|  |     node: true | ||||||
|  |  | ||||||
|  | rules: | ||||||
|  |   comma-dangle: off | ||||||
|  |   "import/order": "error" | ||||||
|  |   "no-unused-vars": "off" | ||||||
|  |   "@typescript-eslint/no-unused-vars": ["error"] | ||||||
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | node_modules | ||||||
|  | .idea | ||||||
|  | .vscode | ||||||
|  | dist | ||||||
							
								
								
									
										6044
									
								
								3CX_TAPI.user.js
									
									
									
									
									
								
							
							
						
						
									
										6044
									
								
								3CX_TAPI.user.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										0
									
								
								config/empty.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								config/empty.js
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								config/metadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								config/metadata.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | const pkg = require('../package.json') | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   name: '3CX TAPI', | ||||||
|  |   namespace: 'http://cp-solutions.at', | ||||||
|  |   version: pkg.version, | ||||||
|  |   author: pkg.author, | ||||||
|  |   copyright: 'Copyright 2020 CP Solutions GmbH', | ||||||
|  |   source: pkg.repository.url, | ||||||
|  |   downloadURL: 'http://scootaloo.cp-austria.at/gitlist/3cx_tapi.git/raw/master/3CX_TAPI.user.js', | ||||||
|  |   match: [ | ||||||
|  |     'https://192.168.0.154:5001/webclient*', | ||||||
|  |     'https://cpsolution.my3cx.at:5001/webclient*' | ||||||
|  |   ], | ||||||
|  |   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', | ||||||
|  |     'GM.notification' | ||||||
|  |   ], | ||||||
|  |   connect: [ | ||||||
|  |     'cpatapi.cpsrvweb2016.cp-austria.at' | ||||||
|  |   ], | ||||||
|  |   'run-at': 'document-end' | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								config/webpack.config.base.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								config/webpack.config.base.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | const path = require('path') | ||||||
|  | const webpack = require('webpack') | ||||||
|  |  | ||||||
|  | const webpackConfig = { | ||||||
|  |   node: { | ||||||
|  |     Buffer: false | ||||||
|  |   }, | ||||||
|  |   resolve: { | ||||||
|  |     extensions: ['.js', '.ts'] | ||||||
|  |   }, | ||||||
|  |   // performance: { | ||||||
|  |   // hints: false | ||||||
|  |   // }, | ||||||
|  |   optimization: { | ||||||
|  |     minimize: false | ||||||
|  |   }, | ||||||
|  |   entry: './src/js/index.js', | ||||||
|  |   output: { | ||||||
|  |     path: path.resolve(__dirname, '../dist') | ||||||
|  |   }, | ||||||
|  |   externals: { | ||||||
|  |     axios: 'axios', | ||||||
|  |     'axios-userscript-adapter': 'axiosGmxhrAdapter' | ||||||
|  |   }, | ||||||
|  |   module: { | ||||||
|  |     rules: [ | ||||||
|  |       { | ||||||
|  |         test: /\.js$/, | ||||||
|  |         exclude: /node_modules/, | ||||||
|  |         loader: 'eslint-loader' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         test: /\.ts$/, | ||||||
|  |         exclude: /node_modules/, | ||||||
|  |         loader: 'ts-loader' | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         test: /\.less$/, | ||||||
|  |         loader: [ | ||||||
|  |           'style-loader', | ||||||
|  |           'css-loader', | ||||||
|  |           'less-loader', // 将 Less 编译为 CSS | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         test: /\.css$/, | ||||||
|  |         loader: [ | ||||||
|  |           'style-loader', | ||||||
|  |           'css-loader', | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   plugins: [ | ||||||
|  |     new webpack.HashedModuleIdsPlugin() | ||||||
|  |   ] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = webpackConfig | ||||||
							
								
								
									
										43
									
								
								config/webpack.config.dev.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								config/webpack.config.dev.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | const { merge } = require("webpack-merge"); | ||||||
|  | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") | ||||||
|  |   .BundleAnalyzerPlugin; | ||||||
|  | const LiveReloadPlugin = require("webpack-livereload-plugin"); | ||||||
|  | const UserScriptMetaDataPlugin = require("userscript-metadata-webpack-plugin"); | ||||||
|  | const metadata = require("./metadata"); | ||||||
|  |  | ||||||
|  | const webpackConfig = require("./webpack.config.base"); | ||||||
|  |  | ||||||
|  | metadata.require.push( | ||||||
|  |   "file://" + path.resolve(__dirname, "../dist/index.prod.user.js") | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const cfg = merge(webpackConfig, { | ||||||
|  |   entry: { | ||||||
|  |     prod: webpackConfig.entry, | ||||||
|  |     dev: path.resolve(__dirname, "./empty.js"), | ||||||
|  |   }, | ||||||
|  |   output: { | ||||||
|  |     filename: "index.[name].user.js", | ||||||
|  |     path: path.resolve(__dirname, "../dist"), | ||||||
|  |   }, | ||||||
|  |   devtool: "inline-source-map", | ||||||
|  |   watch: true, | ||||||
|  |   watchOptions: { | ||||||
|  |     ignored: /node_modules/, | ||||||
|  |   }, | ||||||
|  |   plugins: [ | ||||||
|  |     new LiveReloadPlugin({ | ||||||
|  |       delay: 500, | ||||||
|  |     }), | ||||||
|  |     new UserScriptMetaDataPlugin({ | ||||||
|  |       metadata, | ||||||
|  |     }), | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | if (process.env.npm_config_report) { | ||||||
|  |   cfg.plugins.push(new BundleAnalyzerPlugin()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = cfg; | ||||||
							
								
								
									
										23
									
								
								config/webpack.config.production.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								config/webpack.config.production.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | const { merge } = require("webpack-merge"); | ||||||
|  | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin | ||||||
|  |  | ||||||
|  | const UserScriptMetaDataPlugin = require('userscript-metadata-webpack-plugin') | ||||||
|  | const metadata = require('./metadata') | ||||||
|  |  | ||||||
|  | const webpackConfig = require('./webpack.config.base') | ||||||
|  | const cfg = merge({}, webpackConfig, { | ||||||
|  |   output: { | ||||||
|  |     filename: 'index.prod.user.js' | ||||||
|  |   }, | ||||||
|  |   plugins: [ | ||||||
|  |     new UserScriptMetaDataPlugin({ | ||||||
|  |       metadata | ||||||
|  |     }) | ||||||
|  |   ] | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | if (process.env.npm_config_report) { | ||||||
|  |   cfg.plugins.push(new BundleAnalyzerPlugin()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = cfg | ||||||
							
								
								
									
										8059
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8059
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | { | ||||||
|  |   "name": "3cp-tapi", | ||||||
|  |   "description": "Build your UserScript with webpack", | ||||||
|  |   "version": "7.0.3", | ||||||
|  |   "author": { | ||||||
|  |     "name": "Daniel Triendl", | ||||||
|  |     "email": "d.triendl@cp-solutions.at" | ||||||
|  |   }, | ||||||
|  |   "browserslist": [ | ||||||
|  |     "last 2 version", | ||||||
|  |     "> 1%" | ||||||
|  |   ], | ||||||
|  |   "eslintIgnore": [ | ||||||
|  |     "dist/*.js" | ||||||
|  |   ], | ||||||
|  |   "scripts": { | ||||||
|  |     "lint": "eslint src", | ||||||
|  |     "preversion": "npm run lint", | ||||||
|  |     "postversion": "git push --follow-tags", | ||||||
|  |     "anylize": "npm_config_report=true npm run build", | ||||||
|  |     "build": "webpack --mode production --config config/webpack.config.production.js", | ||||||
|  |     "dev": "webpack --mode development --config config/webpack.config.dev.js" | ||||||
|  |   }, | ||||||
|  |   "repository": { | ||||||
|  |     "type": "git", | ||||||
|  |     "url": "http://scootaloo.cp-austria.at/gitlist/3cx_tapi.git" | ||||||
|  |   }, | ||||||
|  |   "private": true, | ||||||
|  |   "dependencies": { | ||||||
|  |     "axios": "0.20.0", | ||||||
|  |     "axios-userscript-adapter": "0.0.7", | ||||||
|  |     "chrono-node": "2.1.9" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@typescript-eslint/eslint-plugin": "4.3.0", | ||||||
|  |     "@typescript-eslint/parser": "4.3.0", | ||||||
|  |     "css-loader": "4.3.0", | ||||||
|  |     "eslint": "7.10.0", | ||||||
|  |     "eslint-config-standard": "14.1.1", | ||||||
|  |     "eslint-loader": "4.0.2", | ||||||
|  |     "eslint-plugin-import": "2.22.1", | ||||||
|  |     "eslint-plugin-node": "11.1.0", | ||||||
|  |     "eslint-plugin-promise": "4.2.1", | ||||||
|  |     "eslint-plugin-standard": "4.0.1", | ||||||
|  |     "less": "3.12.2", | ||||||
|  |     "less-loader": "7.0.1", | ||||||
|  |     "style-loader": "1.2.1", | ||||||
|  |     "ts-loader": "8.0.4", | ||||||
|  |     "typescript": "4.0.3", | ||||||
|  |     "userscript-metadata-webpack-plugin": "0.0.6", | ||||||
|  |     "webpack": "4.44.2", | ||||||
|  |     "webpack-bundle-analyzer": "3.9.0", | ||||||
|  |     "webpack-cli": "3.3.12", | ||||||
|  |     "webpack-dev-server": "^3.11.0", | ||||||
|  |     "webpack-livereload-plugin": "2.3.0", | ||||||
|  |     "webpack-merge": "5.1.4" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | # 3CX TAPI | ||||||
|  |  | ||||||
|  | Inject CPA TAPI functions into the 3CX Webclient | ||||||
|  |  | ||||||
|  | ## dev | ||||||
|  |  | ||||||
|  | 1. Allow Tampermonkey's access to local file URIs [tampermonkey/faq](https://tampermonkey.net/faq.php?ext=dhdg#Q204) | ||||||
|  | 2. install deps with `npm i` or `npm ci`. | ||||||
|  | 3. `npm run dev` to start your development. | ||||||
|  | 4. open `webpack-userscript-template/dist/index.dev.user.js` in your Chrome and install it with your userscript manager. | ||||||
|  |  | ||||||
|  | this userscript's meta contains `// @require file://path/to/dist/index.prod.user.js`, | ||||||
|  | it will run the code in `index.prod.user.js`, | ||||||
|  | which take [src/js/index.js](./src/js/index.js) as entry point. | ||||||
|  |  | ||||||
|  | every times you edit your metadata, you'll have to install it again, | ||||||
|  | because Tampermonkey don't read it from dist every times. | ||||||
|  |  | ||||||
|  | 5. edit [src/js/index.js](./src/js/index.js) with es6, you can even import css or less files. You can use scss if you like. | ||||||
|  |  | ||||||
|  | livereload is default enabled, use [this chrome extension](https://chrome.google.com/webstore/detail/jnihajbhpnppcggbcgedagnkighmdlei) | ||||||
|  |  | ||||||
|  | ## dependencies | ||||||
|  |  | ||||||
|  | There are two ways to using a package on npm. | ||||||
|  |  | ||||||
|  | ### UserScript way | ||||||
|  |  | ||||||
|  | Like original UserScript way, you will need to add them to your [user script metadata's require section](./config/metadata.js#L13-L17) , and exclude them in [config/webpack.config.base.js](./config/webpack.config.base.js#L21-L25) | ||||||
|  |  | ||||||
|  | ### Webpack way | ||||||
|  |  | ||||||
|  | just install a package and import it in your js file. webpack will pack them with in your final production js file. | ||||||
|  |  | ||||||
|  | ## build | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | npm run build | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | `dist/index.prod.user.js` is the final script. | ||||||
|  |  | ||||||
|  | ## distribution | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | cp dist/index.prod.user.js 3CX_TAPI.user.js | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | And commit 3CX_TAPI.user.js | ||||||
|  |  | ||||||
|  | ## see also | ||||||
|  |  | ||||||
|  | Based on [webpack-userscript-template](https://github.com/Trim21/webpack-userscript-template/) | ||||||
							
								
								
									
										101
									
								
								src/js/call-history.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/js/call-history.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | import * as chrono from 'chrono-node' | ||||||
|  | import { TapiContact } from './tapi-contact' | ||||||
|  | import { axios, extractNumber } from './utils' | ||||||
|  |  | ||||||
|  | export class CallHistory { | ||||||
|  |   private callerIds: { [number: string]: TapiContact } = {} | ||||||
|  |  | ||||||
|  |   private updateCallHistoryEntry (call: HTMLElement, callerId: TapiContact) { | ||||||
|  |     var span = call.querySelector('span') | ||||||
|  |     this.showTimeManager(call, span.nextSibling.textContent.trim(), callerId) | ||||||
|  |  | ||||||
|  |     if (callerId && callerId.tD_NAME !== '') { | ||||||
|  |       var text = span.textContent | ||||||
|  |       span.textContent = callerId.tD_NAME | ||||||
|  |       var br = document.createElement('br') | ||||||
|  |       var span2 = document.createElement('span') | ||||||
|  |       span2.style.fontSize = 'small' | ||||||
|  |       span2.textContent = text | ||||||
|  |       span.parentNode.insertBefore(br, span.nextSibling) | ||||||
|  |       span.parentNode.insertBefore(span2, span.nextSibling) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private showTimeManager (call: HTMLElement, date: string, callerId: TapiContact) { | ||||||
|  |     var dateParts = date.match(/^(?<date>.*), (?<duration>[0-9]{2}:[0-9]{2}:[0-9]{2})$/) | ||||||
|  |     var duration = '00:00:00' | ||||||
|  |     if (dateParts) { | ||||||
|  |       date = dateParts.groups.date | ||||||
|  |       duration = dateParts.groups.duration | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var parsedDate = chrono.de.parseDate(date) | ||||||
|  |     if (!parsedDate) { | ||||||
|  |       parsedDate = chrono.parseDate(date) | ||||||
|  |     } | ||||||
|  |     if (!parsedDate) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     // Date parsing is awful, just assume the first number is the day of month | ||||||
|  |     var day = date.match(/[0-9]+/)[0] | ||||||
|  |  | ||||||
|  |     var parsedDuration = chrono.parseDate(duration) | ||||||
|  |     console.log('TAPI call history time:', date, 'parsedDate:', parsedDate, 'duration:', duration, 'parsedDuration:', parsedDuration) | ||||||
|  |  | ||||||
|  |     var connect = parsedDate.getFullYear().toString() + | ||||||
|  |       (parsedDate.getMonth() + 1).toString().padStart(2, '0') + // (January gives 0) | ||||||
|  |       day.toString().padStart(2, '0') + | ||||||
|  |       parsedDate.getHours().toString().padStart(2, '0') + | ||||||
|  |       parsedDate.getMinutes().toString().padStart(2, '0') | ||||||
|  |  | ||||||
|  |     var length = (parsedDuration.getHours() * 60 + parsedDuration.getMinutes()).toString() | ||||||
|  |  | ||||||
|  |     var toolbar = call.querySelector('.wcToolbarTiles') | ||||||
|  |     var href = 'domizil://PM/Zeitbuchung?' | ||||||
|  |     if (callerId && callerId.tD_ID) { | ||||||
|  |       href += 'KontaktId=' + callerId.tD_ID + '&' | ||||||
|  |     } | ||||||
|  |     href += 'connect=' + connect + '&length=' + length | ||||||
|  |     var a = document.createElement('a') | ||||||
|  |     a.title = 'PM Zeitbuchung' | ||||||
|  |     a.dataset.domizilLink = href | ||||||
|  |     a.onclick = () => { | ||||||
|  |       window.open(href) | ||||||
|  |     } | ||||||
|  |     a.innerHTML = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 559.98 559.98" width="20" height="20">' + | ||||||
|  |       '<g>' + | ||||||
|  |       '  <g>' + | ||||||
|  |       '    <path d="M279.99,0C125.601,0,0,125.601,0,279.99c0,154.39,125.601,279.99,279.99,279.99c154.39,0,279.99-125.601,279.99-279.99' + | ||||||
|  |       '      C559.98,125.601,434.38,0,279.99,0z M279.99,498.78c-120.644,0-218.79-98.146-218.79-218.79' + | ||||||
|  |       '      c0-120.638,98.146-218.79,218.79-218.79s218.79,98.152,218.79,218.79C498.78,400.634,400.634,498.78,279.99,498.78z"/>' + | ||||||
|  |       '    <path d="M304.226,280.326V162.976c0-13.103-10.618-23.721-23.716-23.721c-13.102,0-23.721,10.618-23.721,23.721v124.928' + | ||||||
|  |       '      c0,0.373,0.092,0.723,0.11,1.096c-0.312,6.45,1.91,12.999,6.836,17.926l88.343,88.336c9.266,9.266,24.284,9.266,33.543,0' + | ||||||
|  |       '      c9.26-9.266,9.266-24.284,0-33.544L304.226,280.326z"/>' + | ||||||
|  |       '  </g>' + | ||||||
|  |       '</g>' + | ||||||
|  |       '</svg>' | ||||||
|  |     toolbar.insertBefore(a, toolbar.firstChild) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async showCallHistory (element: HTMLElement) { | ||||||
|  |     var span = element.querySelector('span') | ||||||
|  |     var number = extractNumber(span.textContent) | ||||||
|  |     if (!number) { | ||||||
|  |       this.updateCallHistoryEntry(element, undefined) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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 callerId: TapiContact = { tD_NAME: '' } | ||||||
|  |       if (response.status === 200) { | ||||||
|  |         callerId = response.data | ||||||
|  |       } | ||||||
|  |       console.log('TAPI call histroy callerid response', number, response, callerId) | ||||||
|  |       this.callerIds[number] = callerId | ||||||
|  |       this.updateCallHistoryEntry(element, callerId) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										31
									
								
								src/js/call-notification.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/js/call-notification.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import { TapiContact } from './tapi-contact' | ||||||
|  | import { axios, extractNumber } from './utils' | ||||||
|  |  | ||||||
|  | export class CallNotification { | ||||||
|  |   public async showCallNotification (element: HTMLElement) { | ||||||
|  |     var number = element.dataset.id | ||||||
|  |     console.log('TAPI call notification', number) | ||||||
|  |  | ||||||
|  |     number = extractNumber(number) | ||||||
|  |     if (!number) { | ||||||
|  |       console.log('TAPI callerid no number found') | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     console.log('TAPI searching callerid for', number) | ||||||
|  |     var response = await axios.get<TapiContact>('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 | ||||||
|  |       if (callerId) { | ||||||
|  |         notification.text = callerId.tD_NAME + '\r\n' + number + ' (' + callerId.tD_MEDIUM + ')' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // eslint-disable-next-line no-undef | ||||||
|  |     GM.notification(notification) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								src/js/debounce.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/js/debounce.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | export function debounce (func, wait) { | ||||||
|  |   let timeout | ||||||
|  |  | ||||||
|  |   return function executedFunction (...args) { | ||||||
|  |     const later = () => { | ||||||
|  |       clearTimeout(timeout) | ||||||
|  |       func(...args) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     clearTimeout(timeout) | ||||||
|  |     timeout = setTimeout(later, wait) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								src/js/decs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/js/decs.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | declare module 'axios-userscript-adapter' | ||||||
|  |  | ||||||
|  | declare const GM: any | ||||||
							
								
								
									
										19
									
								
								src/js/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/js/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||||
|  | import * as chrono from 'chrono-node' | ||||||
|  | import { CallHistory } from './call-history' | ||||||
|  | import { CallNotification } from './call-notification' | ||||||
|  | import { Search } from './search' | ||||||
|  |  | ||||||
|  | console.log('script start') | ||||||
|  |  | ||||||
|  | const search = new Search() | ||||||
|  | // eslint-disable-next-line no-undef | ||||||
|  | waitForKeyElements('div.nav-search', (element) => { search.createSearchWindow(element) }, true) | ||||||
|  |  | ||||||
|  | const callNotification = new CallNotification() | ||||||
|  | // eslint-disable-next-line no-undef | ||||||
|  | waitForKeyElements('call-view', (element) => { callNotification.showCallNotification(element) }, false) | ||||||
|  |  | ||||||
|  | const callHistory = new CallHistory() | ||||||
|  | // eslint-disable-next-line no-undef | ||||||
|  | waitForKeyElements('.call-history-list call', (element) => { callHistory.showCallHistory(element) }, false) | ||||||
							
								
								
									
										197
									
								
								src/js/search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/js/search.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | |||||||
|  | import { TapiContact } from './tapi-contact' | ||||||
|  | import { debounce } from './debounce' | ||||||
|  | import { axios } from './utils' | ||||||
|  |  | ||||||
|  | export class Search { | ||||||
|  |   private currentSearchText = '' | ||||||
|  |  | ||||||
|  |   public createSearchWindow (element: HTMLElement) { | ||||||
|  |     console.log('Create TAPI Search') | ||||||
|  |  | ||||||
|  |     var form = document.createElement('form') | ||||||
|  |     form.style.width = '200px' | ||||||
|  |     form.style.float = 'right' | ||||||
|  |     form.style.marginRight = '20px' | ||||||
|  |     form.onsubmit = () => { | ||||||
|  |       var items = document.getElementsByClassName('tapi-search-result-selected') | ||||||
|  |       if (items.length === 0) { | ||||||
|  |         items = document.getElementsByClassName('tapi-search-result') | ||||||
|  |       } | ||||||
|  |       if (items.length > 0) { | ||||||
|  |         this.dial((<HTMLElement>items[0]).dataset.tapiNumber) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return false | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     var searchBox = document.createElement('div') | ||||||
|  |     searchBox.classList.add('contact-search-box') | ||||||
|  |     searchBox.id = 'tapiSearchBox' | ||||||
|  |     form.appendChild(searchBox) | ||||||
|  |  | ||||||
|  |     var searchWrapper = document.createElement('div') | ||||||
|  |     searchWrapper.classList.add('search-input-wrapper') | ||||||
|  |     searchWrapper.style.position = 'relative' | ||||||
|  |     searchBox.appendChild(searchWrapper) | ||||||
|  |  | ||||||
|  |     var search = document.createElement('input') | ||||||
|  |     search.id = 'tapiSearchInput' | ||||||
|  |     search.autocomplete = 'off' | ||||||
|  |     search.classList.add('padder') | ||||||
|  |     search.classList.add('rounded') | ||||||
|  |     search.classList.add('bg-light') | ||||||
|  |     search.classList.add('no-border') | ||||||
|  |     search.classList.add('contact-search-box') | ||||||
|  |     search.placeholder = 'TAPI Suche' | ||||||
|  |     search.onfocus = () => { this.doSearch() } | ||||||
|  |     search.onkeydown = (ev) => { this.doSearchKeyDown(ev) } | ||||||
|  |     search.onblur = () => { | ||||||
|  |       console.log('TAPI Search exit', this) | ||||||
|  |       setTimeout(() => { | ||||||
|  |         console.log('TAPI clear search results') | ||||||
|  |         this.removeSearchResults() | ||||||
|  |       }, 500) | ||||||
|  |     } | ||||||
|  |     searchWrapper.appendChild(search) | ||||||
|  |  | ||||||
|  |     var icon = document.createElement('span') | ||||||
|  |     icon.classList.add('fa') | ||||||
|  |     icon.classList.add('fa-search') | ||||||
|  |     icon.classList.add('form-control-feedback') | ||||||
|  |     icon.style.color = 'grey' | ||||||
|  |     searchWrapper.appendChild(icon) | ||||||
|  |  | ||||||
|  |     element.appendChild(form) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private removeSearchResults () { | ||||||
|  |     var resultList = document.getElementById('tapiResults') | ||||||
|  |     if (resultList) { | ||||||
|  |       resultList.parentNode.removeChild(resultList) | ||||||
|  |     } | ||||||
|  |     this.currentSearchText = '' | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private doSearchKeyDown (ev: KeyboardEvent) { | ||||||
|  |     if (ev.key === 'ArrowUp') { | ||||||
|  |       let items = document.getElementsByClassName('tapi-search-result-selected') | ||||||
|  |       if (items.length > 0) { | ||||||
|  |         var prev = <Element>items[0].previousSibling | ||||||
|  |       } | ||||||
|  |       if (!prev) { | ||||||
|  |         items = document.getElementsByClassName('tapi-search-result') | ||||||
|  |         if (items.length > 0) { | ||||||
|  |           prev = items[items.length - 1] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (prev) { | ||||||
|  |         this.selectResult(prev) | ||||||
|  |         prev.scrollIntoView(true) | ||||||
|  |       } | ||||||
|  |     } else if (ev.key === 'ArrowDown') { | ||||||
|  |       let items = document.getElementsByClassName('tapi-search-result-selected') | ||||||
|  |       if (items.length > 0) { | ||||||
|  |         var next = <Element>items[0].nextSibling | ||||||
|  |       } | ||||||
|  |       if (!next) { | ||||||
|  |         items = document.getElementsByClassName('tapi-search-result') | ||||||
|  |         if (items.length > 0) { | ||||||
|  |           next = items[0] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (next) { | ||||||
|  |         this.selectResult(next) | ||||||
|  |         next.scrollIntoView(false) | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       this.doSearch() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private doSearch = debounce(async () => { | ||||||
|  |     var search = <HTMLInputElement>document.getElementById('tapiSearchInput') | ||||||
|  |     var searchText = search.value.trim() | ||||||
|  |     if (searchText === '') { | ||||||
|  |       this.removeSearchResults() | ||||||
|  |       return | ||||||
|  |     } else if (searchText === this.currentSearchText) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     console.log('Searching TAPI') | ||||||
|  |     var response = await axios.get<TapiContact[]>('http://cpatapi.cpsrvweb2016.cp-austria.at/search?query=' + encodeURIComponent(searchText)) | ||||||
|  |     console.log('TAPI Search response', response) | ||||||
|  |     var contacts = response.data | ||||||
|  |     console.log('TAPI Contacts', contacts) | ||||||
|  |     this.removeSearchResults() | ||||||
|  |     this.currentSearchText = searchText | ||||||
|  |  | ||||||
|  |     var resultList = document.createElement('ul') | ||||||
|  |     resultList.id = 'tapiResults' | ||||||
|  |     resultList.classList.add('search-nav-absolute') | ||||||
|  |     resultList.classList.add('search-nav-ul') | ||||||
|  |     document.getElementById('tapiSearchBox').appendChild(resultList) | ||||||
|  |  | ||||||
|  |     resultList.innerHTML = '' | ||||||
|  |     contacts.forEach(contact => { | ||||||
|  |       var li = document.createElement('li') | ||||||
|  |       li.classList.add('tapi-search-result') | ||||||
|  |       li.classList.add('search-result') | ||||||
|  |       li.classList.add('pointer') | ||||||
|  |       li.onmouseover = () => { this.selectResult(li) } | ||||||
|  |       li.dataset.tapiNumber = contact.tD_NUMBER_TAPI | ||||||
|  |       li.onclick = () => { this.dial(contact.tD_NUMBER_TAPI) } | ||||||
|  |       li.style.listStyle = 'outside none none' // display: flex; align-items: center; | ||||||
|  |  | ||||||
|  |       var resultText = document.createElement('div') | ||||||
|  |       resultText.classList.add('search-result-txt') | ||||||
|  |       li.appendChild(resultText) | ||||||
|  |  | ||||||
|  |       var line1 = document.createElement('div') | ||||||
|  |       line1.appendChild(document.createTextNode(contact.tD_NAME)) | ||||||
|  |       resultText.appendChild(line1) | ||||||
|  |  | ||||||
|  |       var line2 = document.createElement('div') | ||||||
|  |       line2.appendChild(document.createTextNode(contact.tD_MEDIUM + ': ' + contact.tD_NUMBER_TAPI)) | ||||||
|  |       resultText.appendChild(line2) | ||||||
|  |  | ||||||
|  |       resultList.appendChild(li) | ||||||
|  |     }) | ||||||
|  |   }, 200) | ||||||
|  |  | ||||||
|  |   private selectResult (resultLi: Element) { | ||||||
|  |     var items = document.getElementsByClassName('tapi-search-result') | ||||||
|  |     for (var item of items) { | ||||||
|  |       item.classList.remove('bg-light') | ||||||
|  |       item.classList.remove('tapi-search-result-selected') | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resultLi.classList.add('bg-light') | ||||||
|  |     resultLi.classList.add('tapi-search-result-selected') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private dial (number: string) { | ||||||
|  |     var searchInput = document.getElementsByName('searchByNumberInput') | ||||||
|  |     if (searchInput.length > 0) { | ||||||
|  |       (<HTMLInputElement>searchInput[0]).value = number | ||||||
|  |       searchInput[0].focus() | ||||||
|  |  | ||||||
|  |       this.fireChangeEvents(searchInput[0]) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private fireChangeEvents (element: Element) { | ||||||
|  |     var changeEvent = null | ||||||
|  |     changeEvent = document.createEvent('HTMLEvents') | ||||||
|  |     changeEvent.initEvent('input', true, true) | ||||||
|  |     element.dispatchEvent(changeEvent) | ||||||
|  |     console.debug('input event dispatched for element: ' + element.id) | ||||||
|  |     changeEvent = document.createEvent('HTMLEvents') | ||||||
|  |     changeEvent.initEvent('keyup', true, true) | ||||||
|  |     element.dispatchEvent(changeEvent) | ||||||
|  |     console.debug('keyup event dispatched for element: ' + element.id) | ||||||
|  |     changeEvent = document.createEvent('HTMLEvents') | ||||||
|  |     changeEvent.initEvent('change', true, true) | ||||||
|  |     element.dispatchEvent(changeEvent) | ||||||
|  |     console.debug('change event dispatched for element: ' + element.id) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								src/js/tapi-contact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/js/tapi-contact.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | /* eslint-disable camelcase */ | ||||||
|  | export interface TapiContact { | ||||||
|  |   tD_ID?: string; | ||||||
|  |   tD_NAME: string; | ||||||
|  |   tD_NUMBER?: string; | ||||||
|  |   tD_NUMBER_TAPI?: string; | ||||||
|  |   tD_MEDIUM?: string; | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								src/js/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/js/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | |||||||
|  | /** | ||||||
|  |  * @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) { | ||||||
|  |     return undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   var number = match[1] | ||||||
|  |   if (number.startsWith('+')) { | ||||||
|  |     number = number.replace('+', '00') | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return number | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "outDir": "./dist/", | ||||||
|  |     "noImplicitAny": true, | ||||||
|  |     "module": "es6", | ||||||
|  |     "target": "es6", | ||||||
|  |     "allowJs": true, | ||||||
|  |     "moduleResolution": "node" | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user