Merge pull request #1324 from automatisch/placetel
feat(placetel): Implement app structure with authentication
This commit is contained in:
@@ -33,9 +33,11 @@ injectBullBoardHandler(app, serverAdapter);
|
||||
appAssetsHandler(app);
|
||||
|
||||
app.use(morgan);
|
||||
|
||||
app.use(
|
||||
express.json({
|
||||
limit: appConfig.requestBodySizeLimit,
|
||||
type: () => true,
|
||||
verify(req, res, buf) {
|
||||
(req as IRequest).rawBody = buf;
|
||||
},
|
||||
|
6
packages/backend/src/apps/placetel/assets/favicon.svg
Normal file
6
packages/backend/src/apps/placetel/assets/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M45 15c13.807 0 25 11.193 25 25S58.807 65 45 65 20 53.807 20 40s11.193-25 25-25Zm0 14c-6.075 0-11 4.925-11 11s4.925 11 11 11 11-4.925 11-11-4.925-11-11-11Z" fill="#069DD9" fill-rule="nonzero"/>
|
||||
<path fill="#69B52A" d="M20 41h14v33H20z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 368 B |
21
packages/backend/src/apps/placetel/auth/index.ts
Normal file
21
packages/backend/src/apps/placetel/auth/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'apiToken',
|
||||
label: 'API Token',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Placetel API Token of your account.',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
await verifyCredentials($);
|
||||
return true;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,11 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const { data } = await $.http.get('/v2/me');
|
||||
|
||||
await $.auth.set({
|
||||
screenName: `${data.name} @ ${data.company}`,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
11
packages/backend/src/apps/placetel/common/add-auth-header.ts
Normal file
11
packages/backend/src/apps/placetel/common/add-auth-header.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
if ($.auth.data?.apiToken) {
|
||||
requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
3
packages/backend/src/apps/placetel/dynamic-data/index.ts
Normal file
3
packages/backend/src/apps/placetel/dynamic-data/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import listNumbers from './list-numbers';
|
||||
|
||||
export default [listNumbers];
|
@@ -0,0 +1,31 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List numbers',
|
||||
key: 'listNumbers',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const numbers: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const { data } = await $.http.get('/v2/numbers');
|
||||
|
||||
if (!data) {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
if (data.length) {
|
||||
for (const number of data) {
|
||||
numbers.data.push({
|
||||
value: number.number,
|
||||
name: number.number,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return numbers;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/placetel/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/placetel/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/placetel/index.ts
Normal file
20
packages/backend/src/apps/placetel/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Placetel',
|
||||
key: 'placetel',
|
||||
iconUrl: '{BASE_URL}/apps/placetel/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/placetel/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://placetel.de',
|
||||
apiBaseUrl: 'https://api.placetel.de',
|
||||
primaryColor: '069dd9',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
141
packages/backend/src/apps/placetel/triggers/hungup-call/index.ts
Normal file
141
packages/backend/src/apps/placetel/triggers/hungup-call/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import Crypto from 'crypto';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'Hungup Call',
|
||||
key: 'hungupCall',
|
||||
type: 'webhook',
|
||||
description: 'Triggers when a call is hungup.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Types',
|
||||
key: 'types',
|
||||
type: 'dynamic' as const,
|
||||
required: false,
|
||||
description: '',
|
||||
fields: [
|
||||
{
|
||||
label: 'Type',
|
||||
key: 'type',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Filter events by type. If the types are not specified, all types will be notified.',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Voicemail', value: 'voicemail' },
|
||||
{ label: 'Missed', value: 'missed' },
|
||||
{ label: 'Blocked', value: 'blocked' },
|
||||
{ label: 'Accepted', value: 'accepted' },
|
||||
{ label: 'Busy', value: 'busy' },
|
||||
{ label: 'Cancelled', value: 'cancelled' },
|
||||
{ label: 'Unavailable', value: 'unavailable' },
|
||||
{ label: 'Congestion', value: 'congestion' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Numbers',
|
||||
key: 'numbers',
|
||||
type: 'dynamic' as const,
|
||||
required: false,
|
||||
description: '',
|
||||
fields: [
|
||||
{
|
||||
label: 'Number',
|
||||
key: 'number',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Filter events by number. If the numbers are not specified, all numbers will be notified.',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listNumbers',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
let types = ($.step.parameters.types as IJSONObject[]).map(
|
||||
(type) => type.type
|
||||
);
|
||||
|
||||
if (types.length === 0) {
|
||||
types = ['all'];
|
||||
}
|
||||
|
||||
if (types.includes($.request.body.type) || types.includes('all')) {
|
||||
const dataItem = {
|
||||
raw: $.request.body,
|
||||
meta: {
|
||||
internalId: Crypto.randomUUID(),
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
}
|
||||
},
|
||||
|
||||
async testRun($) {
|
||||
const types = ($.step.parameters.types as IJSONObject[]).map(
|
||||
(type) => type.type
|
||||
);
|
||||
|
||||
const sampleEventData = {
|
||||
type: types[0] || 'missed',
|
||||
duration: 0,
|
||||
from: '01662223344',
|
||||
to: '02229997766',
|
||||
call_id:
|
||||
'9c81d4776d3977d920a558cbd4f0950b168e32bd4b5cc141a85b6ed3aa530107',
|
||||
event: 'HungUp',
|
||||
direction: 'in',
|
||||
};
|
||||
|
||||
const dataItem = {
|
||||
raw: sampleEventData,
|
||||
meta: {
|
||||
internalId: sampleEventData.call_id,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
},
|
||||
|
||||
async registerHook($) {
|
||||
const numbers = ($.step.parameters.numbers as IJSONObject[])
|
||||
.map((number: IJSONObject) => number.number)
|
||||
.filter(Boolean);
|
||||
|
||||
const subscriptionPayload = {
|
||||
service: 'string',
|
||||
url: $.webhookUrl,
|
||||
incoming: false,
|
||||
outgoing: false,
|
||||
hungup: true,
|
||||
accepted: false,
|
||||
phone: false,
|
||||
numbers,
|
||||
};
|
||||
|
||||
const { data } = await $.http.put('/v2/subscriptions', subscriptionPayload);
|
||||
|
||||
await $.flow.setRemoteWebhookId(data.id);
|
||||
},
|
||||
|
||||
async unregisterHook($) {
|
||||
await $.http.delete(`/v2/subscriptions/${$.flow.remoteWebhookId}`);
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/placetel/triggers/index.ts
Normal file
3
packages/backend/src/apps/placetel/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import hungupCall from './hungup-call';
|
||||
|
||||
export default [hungupCall];
|
@@ -234,6 +234,15 @@ export default defineConfig({
|
||||
{ text: 'Connection', link: '/apps/pipedrive/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Placetel',
|
||||
collapsible: true,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Triggers', link: '/apps/placetel/triggers' },
|
||||
{ text: 'Connection', link: '/apps/placetel/connection' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'PostgreSQL',
|
||||
collapsible: true,
|
||||
|
7
packages/docs/pages/apps/placetel/connection.md
Normal file
7
packages/docs/pages/apps/placetel/connection.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Placetel
|
||||
|
||||
1. Go to [AppStore page](https://web.placetel.de/integrations) on Placetel.
|
||||
2. Search for `Web API` and click to `Jetzt buchen`.
|
||||
3. Click to `Neuen API-Token erstellen` button and copy the API Token.
|
||||
4. Paste the copied API Token into the `API Token` field in Automatisch.
|
||||
5. Now, you can start using Placetel integration with Automatisch!
|
12
packages/docs/pages/apps/placetel/triggers.md
Normal file
12
packages/docs/pages/apps/placetel/triggers.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
favicon: /favicons/placetel.svg
|
||||
items:
|
||||
- name: Hungup call
|
||||
desc: Triggers when a call is hungup.
|
||||
---
|
||||
|
||||
<script setup>
|
||||
import CustomListing from '../../components/CustomListing.vue'
|
||||
</script>
|
||||
|
||||
<CustomListing />
|
@@ -24,6 +24,7 @@ The following integrations are currently supported by Automatisch.
|
||||
- [Odoo](/apps/odoo/actions)
|
||||
- [OpenAI](/apps/openai/actions)
|
||||
- [Pipedrive](/apps/pipedrive/triggers)
|
||||
- [Placetel](/apps/placetel/triggers)
|
||||
- [PostgreSQL](/apps/postgresql/actions)
|
||||
- [RSS](/apps/rss/triggers)
|
||||
- [Salesforce](/apps/salesforce/triggers)
|
||||
|
6
packages/docs/pages/public/favicons/placetel.svg
Normal file
6
packages/docs/pages/public/favicons/placetel.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 90">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M45 15c13.807 0 25 11.193 25 25S58.807 65 45 65 20 53.807 20 40s11.193-25 25-25Zm0 14c-6.075 0-11 4.925-11 11s4.925 11 11 11 11-4.925 11-11-4.925-11-11-11Z" fill="#069DD9" fill-rule="nonzero"/>
|
||||
<path fill="#69B52A" d="M20 41h14v33H20z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 368 B |
@@ -128,6 +128,36 @@ export const GET_APPS = gql`
|
||||
value
|
||||
}
|
||||
}
|
||||
fields {
|
||||
label
|
||||
key
|
||||
type
|
||||
required
|
||||
description
|
||||
variables
|
||||
value
|
||||
dependsOn
|
||||
options {
|
||||
label
|
||||
value
|
||||
}
|
||||
source {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
additionalFields {
|
||||
type
|
||||
name
|
||||
arguments {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user