diff --git a/packages/backend/src/app.ts b/packages/backend/src/app.ts
index 99c19bf2b8..570427be97 100644
--- a/packages/backend/src/app.ts
+++ b/packages/backend/src/app.ts
@@ -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;
},
diff --git a/packages/backend/src/apps/placetel/assets/favicon.svg b/packages/backend/src/apps/placetel/assets/favicon.svg
new file mode 100644
index 0000000000..6df467ad37
--- /dev/null
+++ b/packages/backend/src/apps/placetel/assets/favicon.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/backend/src/apps/placetel/auth/index.ts b/packages/backend/src/apps/placetel/auth/index.ts
new file mode 100644
index 0000000000..6ce142f0f9
--- /dev/null
+++ b/packages/backend/src/apps/placetel/auth/index.ts
@@ -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,
+};
diff --git a/packages/backend/src/apps/placetel/auth/is-still-verified.ts b/packages/backend/src/apps/placetel/auth/is-still-verified.ts
new file mode 100644
index 0000000000..66bb963ead
--- /dev/null
+++ b/packages/backend/src/apps/placetel/auth/is-still-verified.ts
@@ -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;
diff --git a/packages/backend/src/apps/placetel/auth/verify-credentials.ts b/packages/backend/src/apps/placetel/auth/verify-credentials.ts
new file mode 100644
index 0000000000..7fab269f28
--- /dev/null
+++ b/packages/backend/src/apps/placetel/auth/verify-credentials.ts
@@ -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;
diff --git a/packages/backend/src/apps/placetel/common/add-auth-header.ts b/packages/backend/src/apps/placetel/common/add-auth-header.ts
new file mode 100644
index 0000000000..8a77a4976e
--- /dev/null
+++ b/packages/backend/src/apps/placetel/common/add-auth-header.ts
@@ -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;
diff --git a/packages/backend/src/apps/placetel/dynamic-data/index.ts b/packages/backend/src/apps/placetel/dynamic-data/index.ts
new file mode 100644
index 0000000000..819a758d6c
--- /dev/null
+++ b/packages/backend/src/apps/placetel/dynamic-data/index.ts
@@ -0,0 +1,3 @@
+import listNumbers from './list-numbers';
+
+export default [listNumbers];
diff --git a/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.ts b/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.ts
new file mode 100644
index 0000000000..c3dff3a7d3
--- /dev/null
+++ b/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.ts
@@ -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;
+ },
+};
diff --git a/packages/backend/src/apps/placetel/index.d.ts b/packages/backend/src/apps/placetel/index.d.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/backend/src/apps/placetel/index.ts b/packages/backend/src/apps/placetel/index.ts
new file mode 100644
index 0000000000..f682ea2691
--- /dev/null
+++ b/packages/backend/src/apps/placetel/index.ts
@@ -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,
+});
diff --git a/packages/backend/src/apps/placetel/triggers/hungup-call/index.ts b/packages/backend/src/apps/placetel/triggers/hungup-call/index.ts
new file mode 100644
index 0000000000..ac466a7573
--- /dev/null
+++ b/packages/backend/src/apps/placetel/triggers/hungup-call/index.ts
@@ -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}`);
+ },
+});
diff --git a/packages/backend/src/apps/placetel/triggers/index.ts b/packages/backend/src/apps/placetel/triggers/index.ts
new file mode 100644
index 0000000000..9a8974d82d
--- /dev/null
+++ b/packages/backend/src/apps/placetel/triggers/index.ts
@@ -0,0 +1,3 @@
+import hungupCall from './hungup-call';
+
+export default [hungupCall];
diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js
index 1e1c0c608f..a1a77c5a03 100644
--- a/packages/docs/pages/.vitepress/config.js
+++ b/packages/docs/pages/.vitepress/config.js
@@ -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,
diff --git a/packages/docs/pages/apps/placetel/connection.md b/packages/docs/pages/apps/placetel/connection.md
new file mode 100644
index 0000000000..a0f0728ee7
--- /dev/null
+++ b/packages/docs/pages/apps/placetel/connection.md
@@ -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!
diff --git a/packages/docs/pages/apps/placetel/triggers.md b/packages/docs/pages/apps/placetel/triggers.md
new file mode 100644
index 0000000000..99c86c1ce8
--- /dev/null
+++ b/packages/docs/pages/apps/placetel/triggers.md
@@ -0,0 +1,12 @@
+---
+favicon: /favicons/placetel.svg
+items:
+ - name: Hungup call
+ desc: Triggers when a call is hungup.
+---
+
+
+
+
diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md
index 29b5e7f8c1..f293d57e4c 100644
--- a/packages/docs/pages/guide/available-apps.md
+++ b/packages/docs/pages/guide/available-apps.md
@@ -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)
diff --git a/packages/docs/pages/public/favicons/placetel.svg b/packages/docs/pages/public/favicons/placetel.svg
new file mode 100644
index 0000000000..6df467ad37
--- /dev/null
+++ b/packages/docs/pages/public/favicons/placetel.svg
@@ -0,0 +1,6 @@
+
diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts
index de417dacb1..3cfcdd1741 100644
--- a/packages/web/src/graphql/queries/get-apps.ts
+++ b/packages/web/src/graphql/queries/get-apps.ts
@@ -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
+ }
+ }
+ }
}
}
}