Compare commits
317 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
142b96beb0 | ||
![]() |
18d07dd3b9 | ||
![]() |
270039d211 | ||
![]() |
fcf3a480be | ||
![]() |
80ee974973 | ||
![]() |
48c6a38205 | ||
![]() |
c9536e58cb | ||
![]() |
a16d783302 | ||
![]() |
b837aecb27 | ||
![]() |
532405048f | ||
![]() |
6cd0427ff8 | ||
![]() |
d2163f180e | ||
![]() |
d1344457dd | ||
![]() |
596be24d92 | ||
![]() |
4e182b49f9 | ||
![]() |
51ac37f1de | ||
![]() |
b578e88d46 | ||
![]() |
9cb4607f69 | ||
![]() |
2c4d602028 | ||
![]() |
a0feb7f309 | ||
![]() |
77e29050c8 | ||
![]() |
df55f746ca | ||
![]() |
2739d2297f | ||
![]() |
dc734b04d8 | ||
![]() |
d4c542168c | ||
![]() |
94a3b66130 | ||
![]() |
c8baf9b0d7 | ||
![]() |
c4dc0509c2 | ||
![]() |
3bfc428dfe | ||
![]() |
026b7da6f8 | ||
![]() |
58352b9f33 | ||
![]() |
59faf593be | ||
![]() |
4aa41773c4 | ||
![]() |
20c5d6aee1 | ||
![]() |
d503dbc103 | ||
![]() |
53624a6379 | ||
![]() |
5b881db19f | ||
![]() |
dece070d28 | ||
![]() |
8be2ed0034 | ||
![]() |
37c6b57a48 | ||
![]() |
8a0b5c24a5 | ||
![]() |
20431da757 | ||
![]() |
f219c87a9e | ||
![]() |
5d2134db56 | ||
![]() |
1dc46dd31c | ||
![]() |
96250ce9a0 | ||
![]() |
691682e09c | ||
![]() |
814c504951 | ||
![]() |
5c2b96a812 | ||
![]() |
3dc1ca8adb | ||
![]() |
a6850f1bc0 | ||
![]() |
5ba6cb9135 | ||
![]() |
500283af6f | ||
![]() |
43184dccda | ||
![]() |
4296a3b5df | ||
![]() |
14ba50f061 | ||
![]() |
9ad7de56a3 | ||
![]() |
5570687957 | ||
![]() |
1406bf3e10 | ||
![]() |
287f299b94 | ||
![]() |
f88f05cd46 | ||
![]() |
c6fb42e2f3 | ||
![]() |
c64ca9d9b7 | ||
![]() |
7d47793afb | ||
![]() |
2840fce856 | ||
![]() |
2791dca412 | ||
![]() |
3e268bf66b | ||
![]() |
f44422a77e | ||
![]() |
4931cbcc34 | ||
![]() |
59e495f7d3 | ||
![]() |
93866d7bc2 | ||
![]() |
ba1cdeaeeb | ||
![]() |
234b27e555 | ||
![]() |
5acfd09819 | ||
![]() |
9923b15ecd | ||
![]() |
def5a6d9d0 | ||
![]() |
17c8a405f5 | ||
![]() |
ff874bfb48 | ||
![]() |
4cbb37b996 | ||
![]() |
f57e35c6b8 | ||
![]() |
312e786e33 | ||
![]() |
e2c75a2daf | ||
![]() |
fce20263ea | ||
![]() |
d841f9cb62 | ||
![]() |
4945240ec2 | ||
![]() |
f1cdc7e422 | ||
![]() |
372cdb10d6 | ||
![]() |
fd24dbee21 | ||
![]() |
7066f7ea76 | ||
![]() |
5ae53c79b6 | ||
![]() |
f3a8ab289f | ||
![]() |
a99609e3da | ||
![]() |
a271189448 | ||
![]() |
251bc28526 | ||
![]() |
05505704e4 | ||
![]() |
4aadcb021e | ||
![]() |
ae9da1b354 | ||
![]() |
a594029541 | ||
![]() |
ddcb894932 | ||
![]() |
708cdbe545 | ||
![]() |
1cbf96dff1 | ||
![]() |
bd55b37d5f | ||
![]() |
7c3a0effee | ||
![]() |
fe3048aab0 | ||
![]() |
9a5a3e879d | ||
![]() |
269902db94 | ||
![]() |
4ba67ea863 | ||
![]() |
3534712478 | ||
![]() |
62b9a8071a | ||
![]() |
cf83c27ca0 | ||
![]() |
8035f81f97 | ||
![]() |
d2375fdc54 | ||
![]() |
f07d9dd813 | ||
![]() |
b5524b18cf | ||
![]() |
310497a5bf | ||
![]() |
47a738d5e6 | ||
![]() |
6ad13c1da0 | ||
![]() |
99454fdc4b | ||
![]() |
799f0ead6c | ||
![]() |
d16d709b72 | ||
![]() |
8e1087b818 | ||
![]() |
139dcb521e | ||
![]() |
b17e431473 | ||
![]() |
b1ee3ef8ba | ||
![]() |
75bbd16b0c | ||
![]() |
71a7943d01 | ||
![]() |
d34785b5b0 | ||
![]() |
af58ef7244 | ||
![]() |
6d3bec8518 | ||
![]() |
0346b157c5 | ||
![]() |
e25aab742b | ||
![]() |
6f2ca00263 | ||
![]() |
e1d26325f3 | ||
![]() |
4202f963c3 | ||
![]() |
d3ef45db1b | ||
![]() |
dec55709a3 | ||
![]() |
b5ed984f05 | ||
![]() |
66d7baa126 | ||
![]() |
a2809a14c5 | ||
![]() |
040ad9edb0 | ||
![]() |
46dbe009f2 | ||
![]() |
3598d43938 | ||
![]() |
f1358c7ad1 | ||
![]() |
40862fcd01 | ||
![]() |
189432c228 | ||
![]() |
280d16f3d9 | ||
![]() |
722c39590f | ||
![]() |
b1138dbf05 | ||
![]() |
08918282a7 | ||
![]() |
5007b0bf1a | ||
![]() |
0a5912eb8e | ||
![]() |
8acd7b03ed | ||
![]() |
ed87df212f | ||
![]() |
56243aa076 | ||
![]() |
41e9f32e1b | ||
![]() |
f7753aa1b4 | ||
![]() |
aebfcc38dd | ||
![]() |
3e0149c058 | ||
![]() |
1e62e09825 | ||
![]() |
956b15a2eb | ||
![]() |
572b457b43 | ||
![]() |
7d40ae009f | ||
![]() |
aec9595dea | ||
![]() |
cc90f19a46 | ||
![]() |
ab486c8ed1 | ||
![]() |
bfb1e817ec | ||
![]() |
ebc7c22388 | ||
![]() |
10b4066c82 | ||
![]() |
42dd67954d | ||
![]() |
d45fdf605f | ||
![]() |
caef9bb8b5 | ||
![]() |
c888934601 | ||
![]() |
95613b595e | ||
![]() |
9247bd9d9e | ||
![]() |
ee3d2489e6 | ||
![]() |
7764c4fbcb | ||
![]() |
a18b524859 | ||
![]() |
9b8ec9b85e | ||
![]() |
7b3f070973 | ||
![]() |
ba27fc12e8 | ||
![]() |
db55912f78 | ||
![]() |
4c4bd267d4 | ||
![]() |
dc1002659b | ||
![]() |
97da370301 | ||
![]() |
571901f333 | ||
![]() |
4d90df9d9a | ||
![]() |
2c18667ffd | ||
![]() |
89157cd606 | ||
![]() |
37e0091ef0 | ||
![]() |
ae3512fecf | ||
![]() |
8b81391e2f | ||
![]() |
54e68f6252 | ||
![]() |
92d1ed65ff | ||
![]() |
8098a7ee5d | ||
![]() |
c24297630c | ||
![]() |
a9d5212602 | ||
![]() |
c81db8ae19 | ||
![]() |
ecd356d42b | ||
![]() |
e67adf87b2 | ||
![]() |
1669708041 | ||
![]() |
12e34013f8 | ||
![]() |
bd2ad1d7a1 | ||
![]() |
934cdb8237 | ||
![]() |
2f6ea8830e | ||
![]() |
b77d08ebbf | ||
![]() |
b735d32cbc | ||
![]() |
472ffd5b5c | ||
![]() |
4567ca8fce | ||
![]() |
07ed24ca7a | ||
![]() |
52575be2a7 | ||
![]() |
c8187e52bb | ||
![]() |
b0b6b72b4c | ||
![]() |
7676bc5836 | ||
![]() |
f98c1725be | ||
![]() |
c6bd599b63 | ||
![]() |
7908779c89 | ||
![]() |
29ad68afab | ||
![]() |
9aaeac6a08 | ||
![]() |
8f074c2131 | ||
![]() |
930653c86d | ||
![]() |
cc31b7c210 | ||
![]() |
bb19e9308c | ||
![]() |
5e7b4bfe45 | ||
![]() |
dbeeb61cc5 | ||
![]() |
26d8e5856a | ||
![]() |
66be6d1e89 | ||
![]() |
677aa232e7 | ||
![]() |
cabbb45031 | ||
![]() |
ba99df645b | ||
![]() |
282e5ba2d8 | ||
![]() |
42d418da58 | ||
![]() |
cbf270fdba | ||
![]() |
bb9abe104f | ||
![]() |
18f0d6dea3 | ||
![]() |
23dc9a1139 | ||
![]() |
5e18ef5830 | ||
![]() |
63f8fc266d | ||
![]() |
4e46b16f7b | ||
![]() |
ba19b50005 | ||
![]() |
fa867387d4 | ||
![]() |
5762cf5dc5 | ||
![]() |
7d9f624805 | ||
![]() |
5b335ccd59 | ||
![]() |
4792853eb6 | ||
![]() |
cc05bc7db8 | ||
![]() |
d198eaa988 | ||
![]() |
f094da6a4b | ||
![]() |
a53961b235 | ||
![]() |
f644113af8 | ||
![]() |
e4a903ec07 | ||
![]() |
9408fe2a07 | ||
![]() |
394e747a88 | ||
![]() |
e91f12729b | ||
![]() |
134e588b14 | ||
![]() |
d061eb7b58 | ||
![]() |
18089a8076 | ||
![]() |
02236e01d9 | ||
![]() |
c1150d50b1 | ||
![]() |
387a849269 | ||
![]() |
399cebda70 | ||
![]() |
72720b3dfe | ||
![]() |
64f7560b3b | ||
![]() |
e06b646f49 | ||
![]() |
b51d9bb17b | ||
![]() |
4e967c5720 | ||
![]() |
179db38fd1 | ||
![]() |
e51930d7e1 | ||
![]() |
74a299dbe6 | ||
![]() |
282863c526 | ||
![]() |
63d794ed3e | ||
![]() |
c6b8f12f9a | ||
![]() |
d16e292231 | ||
![]() |
29a319a850 | ||
![]() |
c11e0db077 | ||
![]() |
b2dafb5dfa | ||
![]() |
e7ccd01427 | ||
![]() |
447f26458a | ||
![]() |
61fb71a080 | ||
![]() |
329fcdf8f4 | ||
![]() |
37cdb34014 | ||
![]() |
5ee7b85cc4 | ||
![]() |
1f7228f95a | ||
![]() |
fa8418adcd | ||
![]() |
d3b1765ffe | ||
![]() |
0a274ebadb | ||
![]() |
0d1b35edc5 | ||
![]() |
d7e4ae53ce | ||
![]() |
61ceffc6f9 | ||
![]() |
0c422bfd21 | ||
![]() |
a0815b06a6 | ||
![]() |
bd02b7574a | ||
![]() |
1048d923d0 | ||
![]() |
9dbfcf4262 | ||
![]() |
8ea176b5f0 | ||
![]() |
90dcbadc52 | ||
![]() |
e4021bf830 | ||
![]() |
b8b453aba0 | ||
![]() |
aeec2377c1 | ||
![]() |
f8b0ffd39b | ||
![]() |
9953c3c823 | ||
![]() |
d227a07fe9 | ||
![]() |
7c394414d8 | ||
![]() |
1dfb22d02e | ||
![]() |
1847ad5622 | ||
![]() |
beb701ceb4 | ||
![]() |
2fbadea821 | ||
![]() |
bac561d8c7 | ||
![]() |
7bd261a02e | ||
![]() |
df99a889f0 | ||
![]() |
16c5892a1d | ||
![]() |
0236bbaf68 | ||
![]() |
2ca5a290c3 | ||
![]() |
66c388a644 | ||
![]() |
74a77ed271 | ||
![]() |
3d98e6cdc0 | ||
![]() |
831ae96e0f | ||
![]() |
113a91a73f |
@@ -8,10 +8,8 @@ echo "Configuring backend environment variables..."
|
||||
cd packages/backend
|
||||
rm -rf .env
|
||||
echo "
|
||||
HOST=localhost
|
||||
PROTOCOL=http
|
||||
PORT=$BACKEND_PORT
|
||||
WEB_APP_URL=https://$CODESPACE_NAME-$WEB_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
|
||||
WEB_APP_URL=http://localhost:$WEB_PORT
|
||||
APP_ENV=development
|
||||
POSTGRES_DATABASE=automatisch
|
||||
POSTGRES_PORT=5432
|
||||
@@ -30,8 +28,7 @@ cd packages/web
|
||||
rm -rf .env
|
||||
echo "
|
||||
PORT=$WEB_PORT
|
||||
REACT_APP_GRAPHQL_URL=https://$CODESPACE_NAME-$BACKEND_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/graphql
|
||||
REACT_APP_BASE_URL=https://$CODESPACE_NAME-$WEB_PORT.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
|
||||
REACT_APP_GRAPHQL_URL=http://localhost:$BACKEND_PORT/graphql
|
||||
REACT_APP_NOTIFICATIONS_URL=https://notifications.automatisch.io
|
||||
" >> .env
|
||||
cd $CURRENT_DIR
|
||||
|
@@ -21,10 +21,18 @@ services:
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports:
|
||||
- '5432:5432'
|
||||
expose:
|
||||
- 5432
|
||||
redis:
|
||||
image: 'redis:7.0.4-alpine'
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- '6379:6379'
|
||||
expose:
|
||||
- 6379
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
5
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal file
5
CONTRIBUTOR_LICENSE_AGREEMENT.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Automatisch Contributor License Agreement
|
||||
|
||||
I give Automatisch permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project.
|
||||
|
||||
**_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._**
|
3
LICENSE
Normal file
3
LICENSE
Normal file
@@ -0,0 +1,3 @@
|
||||
LICENSE.agpl (AGPL-3.0) applies to all files in this
|
||||
repository, except for files that contain ".ee." in their name
|
||||
which are covered by LICENSE.enterprise.
|
35
LICENSE.enterprise
Normal file
35
LICENSE.enterprise
Normal file
@@ -0,0 +1,35 @@
|
||||
The Automatisch Enterprise license (the “Enterprise License”)
|
||||
Copyright (c) 2023 Ömer Faruk Aydın, Ali Barın.
|
||||
|
||||
With regard to the Automatisch Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have a valid
|
||||
Automatisch Enterprise license for the correct number of user seats. Subject
|
||||
to the foregoing sentence, you are free to modify this Software and publish
|
||||
patches to the Software. You agree that Automatisch and/or its licensors
|
||||
(as applicable) retain all right, title and interest in and to all such
|
||||
modifications and/or patches, and all such modifications and/or patches may
|
||||
only be used, copied, modified, displayed, distributed, or otherwise exploited
|
||||
with a valid Automatisch Enterprise license for the correct number of user seats.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for
|
||||
development and testing purposes, without requiring a subscription. You agree
|
||||
that Automatisch and/or its licensors (as applicable) retain all right, title
|
||||
and interest in and to all such modifications. You are not granted any other
|
||||
rights beyond what is expressly stated herein. Subject to the foregoing, it is
|
||||
forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software.
|
||||
|
||||
The full text of this Enterprise License shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Automatisch Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
14
README.md
14
README.md
@@ -44,10 +44,18 @@ For other installation types, you can check the [installation](https://automatis
|
||||
|
||||
## Support
|
||||
|
||||
If you have any questions or problems, please visit our GitHub discussions page, and we'll try to help you as soon as possible.
|
||||
If you have any questions or problems, please visit our GitHub issues page, and we'll try to help you as soon as possible.
|
||||
|
||||
[https://github.com/automatisch/automatisch/discussions](https://github.com/automatisch/automatisch/discussions)
|
||||
[https://github.com/automatisch/automatisch/issues](https://github.com/automatisch/automatisch/issues)
|
||||
|
||||
## License
|
||||
|
||||
Automatisch is an open-source software with the [AGPL 3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.md).
|
||||
Automatisch Community Edition (Automatisch CE) is an open-source software with the [AGPL-3.0 license](LICENSE.agpl).
|
||||
|
||||
Automatisch Enterprise Edition (Automatisch EE) is a commercial offering with the [Enterprise license](LICENSE.enterprise).
|
||||
|
||||
The Automatisch repository contains both AGPL-licensed and Enterprise-licensed files. We maintain a single repository to make development easier.
|
||||
|
||||
All files that contain ".ee." in their name fall under the [Enterprise license](LICENSE.enterprise). All other files fall under the [AGPL-3.0 license](LICENSE.agpl).
|
||||
|
||||
See the [LICENSE](LICENSE) file for more information.
|
||||
|
@@ -2,13 +2,13 @@
|
||||
FROM node:16-alpine
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apk --no-cache add --virtual build-dependencies python3 build-base
|
||||
RUN \
|
||||
apk --no-cache add --virtual build-dependencies python3 build-base && \
|
||||
yarn global add @automatisch/cli@0.5.0 --network-timeout 1000000 && \
|
||||
rm -rf /usr/local/share/.cache/ && \
|
||||
apk del build-dependencies
|
||||
|
||||
COPY ./entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN yarn global add @automatisch/cli@0.4.0 --network-timeout 1000000
|
||||
|
||||
RUN apk del build-dependencies python3 build-base
|
||||
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||
|
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM automatischio/automatisch:0.4.0
|
||||
FROM automatischio/automatisch:0.5.0
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apk add --no-cache openssl dos2unix
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/root",
|
||||
"license": "AGPL-3.0",
|
||||
"license": "See LICENSE file",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||
|
@@ -12,6 +12,8 @@ export async function createUser(
|
||||
const userParams = {
|
||||
email,
|
||||
password,
|
||||
fullName: 'Initial admin',
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
try {
|
||||
|
@@ -12,6 +12,7 @@ const knexConfig = {
|
||||
database: appConfig.postgresDatabase,
|
||||
ssl: appConfig.postgresEnableSsl,
|
||||
},
|
||||
searchPath: [appConfig.postgresSchema],
|
||||
pool: { min: 0, max: 20 },
|
||||
migrations: {
|
||||
directory: __dirname + '/src/db/migrations',
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.4.0",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.6.0",
|
||||
"license": "See LICENSE file",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --exit-child src/server.ts",
|
||||
@@ -22,11 +22,13 @@
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "^0.4.0",
|
||||
"@automatisch/web": "^0.6.0",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
"@graphql-tools/load": "^7.5.2",
|
||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||
"@sentry/node": "^7.42.0",
|
||||
"@sentry/tracing": "^7.42.0",
|
||||
"@types/luxon": "^2.3.1",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"axios": "0.24.0",
|
||||
@@ -45,17 +47,21 @@
|
||||
"graphql-shield": "^7.5.0",
|
||||
"graphql-tools": "^8.2.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"http-errors": "~1.6.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"knex": "^2.4.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"luxon": "2.5.2",
|
||||
"memory-cache": "^0.2.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.0.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
"stripe": "^11.13.0",
|
||||
"winston": "^3.7.1"
|
||||
},
|
||||
"contributors": [
|
||||
@@ -94,7 +100,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "^0.4.0",
|
||||
"@automatisch/types": "^0.6.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -103,6 +109,7 @@
|
||||
"@types/http-errors": "^1.8.1",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/memory-cache": "^0.2.2",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "1.4.7",
|
||||
"@types/node": "^16.10.2",
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import createError from 'http-errors';
|
||||
import express from 'express';
|
||||
import appConfig from './config/app';
|
||||
import cors from 'cors';
|
||||
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import appConfig from './config/app';
|
||||
import corsOptions from './config/cors-options';
|
||||
import morgan from './helpers/morgan';
|
||||
import * as Sentry from './helpers/sentry.ee';
|
||||
import appAssetsHandler from './helpers/app-assets-handler';
|
||||
import webUIHandler from './helpers/web-ui-handler';
|
||||
import errorHandler from './helpers/error-handler';
|
||||
@@ -14,12 +17,16 @@ import {
|
||||
} from './helpers/create-bull-board-handler';
|
||||
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
||||
import router from './routes';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
|
||||
createBullBoardHandler(serverAdapter);
|
||||
|
||||
const app = express();
|
||||
|
||||
Sentry.init(app);
|
||||
|
||||
Sentry.attachRequestHandler(app);
|
||||
Sentry.attachTracingHandler(app);
|
||||
|
||||
injectBullBoardHandler(app, serverAdapter);
|
||||
|
||||
appAssetsHandler(app);
|
||||
@@ -33,13 +40,15 @@ app.use(
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(express.urlencoded({
|
||||
extended: false,
|
||||
limit: appConfig.requestBodySizeLimit,
|
||||
verify(req, res, buf) {
|
||||
(req as IRequest).rawBody = buf;
|
||||
},
|
||||
}));
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
limit: appConfig.requestBodySizeLimit,
|
||||
verify(req, res, buf) {
|
||||
(req as IRequest).rawBody = buf;
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(cors(corsOptions));
|
||||
app.use('/', router);
|
||||
|
||||
@@ -50,6 +59,8 @@ app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
Sentry.attachErrorHandler(app);
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
export default app;
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import path from 'node:path';
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create folder',
|
||||
key: 'createFolder',
|
||||
description: 'Create a new folder with the given parent folder and folder name',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Folder',
|
||||
key: 'parentFolder',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Enter the parent folder path, like /TextFiles/ or /Documents/Taxes/',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Folder Name',
|
||||
key: 'folderName',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Enter the name for the new folder',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const parentFolder = $.step.parameters.parentFolder as string;
|
||||
const folderName = $.step.parameters.folderName as string;
|
||||
const folderPath = path.join(parentFolder, folderName);
|
||||
|
||||
const response = await $.http.post('/2/files/create_folder_v2', { path: folderPath });
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
4
packages/backend/src/apps/dropbox/actions/index.ts
Normal file
4
packages/backend/src/apps/dropbox/actions/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import createFolder from "./create-folder";
|
||||
import renameFile from "./rename-file";
|
||||
|
||||
export default [createFolder, renameFile];
|
@@ -0,0 +1,45 @@
|
||||
import path from 'node:path';
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Rename file',
|
||||
key: 'renameFile',
|
||||
description: 'Rename a file with the given file path and new name',
|
||||
arguments: [
|
||||
{
|
||||
label: 'File Path',
|
||||
key: 'filePath',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'Write the full path to the file such as /Folder1/File.pdf',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'New Name',
|
||||
key: 'newName',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: "Enter the new name for the file (without the extension, e.g., '.pdf')",
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const filePath = $.step.parameters.filePath as string;
|
||||
const newName = $.step.parameters.newName as string;
|
||||
const fileObject = path.parse(filePath);
|
||||
const newPath = path.format({
|
||||
dir: fileObject.dir,
|
||||
ext: fileObject.ext,
|
||||
name: newName,
|
||||
});
|
||||
|
||||
const response = await $.http.post('/2/files/move_v2', {
|
||||
from_path: filePath,
|
||||
to_path: newPath,
|
||||
});
|
||||
|
||||
$.setActionItem({ raw: response.data.metadata });
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/dropbox/assets/favicon.svg
Normal file
3
packages/backend/src/apps/dropbox/assets/favicon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Dropbox" role="img" viewBox="0 0 512 512" fill="#0061ff">
|
||||
<path d="M158 101l-99 63 295 188 99-63m-99-188l99 63-295 188-99-63m99 83l98 63 98-63-98-62z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 213 B |
22
packages/backend/src/apps/dropbox/auth/generate-auth-url.ts
Normal file
22
packages/backend/src/apps/dropbox/auth/generate-auth-url.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { URLSearchParams } from 'url';
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import scopes from '../common/scopes';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const callbackUrl = oauthRedirectUrlField.value as string;
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
redirect_uri: callbackUrl,
|
||||
response_type: 'code',
|
||||
scope: scopes.join(' '),
|
||||
token_access_type: 'offline',
|
||||
});
|
||||
|
||||
const url = `${$.app.baseUrl}/oauth2/authorize?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({ url });
|
||||
}
|
48
packages/backend/src/apps/dropbox/auth/index.ts
Normal file
48
packages/backend/src/apps/dropbox/auth/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
import refreshToken from './refresh-token';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/dropbox/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input an OAuth callback or redirect URL in Dropbox OAuth, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'App Key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'App Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentAccount from '../common/get-current-account';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
const account = await getCurrentAccount($);
|
||||
return !!account;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
41
packages/backend/src/apps/dropbox/auth/refresh-token.ts
Normal file
41
packages/backend/src/apps/dropbox/auth/refresh-token.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const refreshToken = async ($: IGlobalVariable) => {
|
||||
const params = {
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: $.auth.data.refreshToken as string,
|
||||
};
|
||||
|
||||
const basicAuthToken = Buffer
|
||||
.from(`${$.auth.data.clientId}:${$.auth.data.clientSecret}`)
|
||||
.toString('base64');
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'oauth2/token',
|
||||
null,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Basic ${basicAuthToken}`
|
||||
},
|
||||
additionalProperties: {
|
||||
skipAddingAuthHeader: true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
access_token: accessToken,
|
||||
expires_in: expiresIn,
|
||||
token_type: tokenType,
|
||||
} = data;
|
||||
|
||||
await $.auth.set({
|
||||
accessToken,
|
||||
expiresIn,
|
||||
tokenType,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
102
packages/backend/src/apps/dropbox/auth/verify-credentials.ts
Normal file
102
packages/backend/src/apps/dropbox/auth/verify-credentials.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IGlobalVariable, IField } from '@automatisch/types';
|
||||
import getCurrentAccount from '../common/get-current-account';
|
||||
|
||||
type TAccount = {
|
||||
account_id: string,
|
||||
name: {
|
||||
given_name: string,
|
||||
surname: string,
|
||||
familiar_name: string,
|
||||
display_name: string,
|
||||
abbreviated_name: string,
|
||||
},
|
||||
email: string,
|
||||
email_verified: boolean,
|
||||
disabled: boolean,
|
||||
country: string,
|
||||
locale: string,
|
||||
referral_link: string,
|
||||
is_paired: boolean,
|
||||
account_type: {
|
||||
".tag": string,
|
||||
},
|
||||
root_info: {
|
||||
".tag": string,
|
||||
root_namespace_id: string,
|
||||
home_namespace_id: string,
|
||||
},
|
||||
}
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUrl = oauthRedirectUrlField.value as string;
|
||||
const params = {
|
||||
client_id: $.auth.data.clientId as string,
|
||||
redirect_uri: redirectUrl,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
code: $.auth.data.code as string,
|
||||
grant_type: 'authorization_code',
|
||||
}
|
||||
const { data: verifiedCredentials } = await $.http.post(
|
||||
'/oauth2/token',
|
||||
null,
|
||||
{ params }
|
||||
);
|
||||
|
||||
const {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
expires_in: expiresIn,
|
||||
scope: scope,
|
||||
token_type: tokenType,
|
||||
account_id: accountId,
|
||||
team_id: teamId,
|
||||
id_token: idToken,
|
||||
uid,
|
||||
} = verifiedCredentials;
|
||||
|
||||
await $.auth.set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
scope,
|
||||
tokenType,
|
||||
accountId,
|
||||
teamId,
|
||||
idToken,
|
||||
uid
|
||||
});
|
||||
|
||||
const account = await getCurrentAccount($) as TAccount;
|
||||
|
||||
await $.auth.set({
|
||||
accountId: account.account_id,
|
||||
name: {
|
||||
givenName: account.name.given_name,
|
||||
surname: account.name.surname,
|
||||
familiarName: account.name.familiar_name,
|
||||
displayName: account.name.display_name,
|
||||
abbreviatedName: account.name.abbreviated_name,
|
||||
},
|
||||
email: account.email,
|
||||
emailVerified: account.email_verified,
|
||||
disabled: account.disabled,
|
||||
country: account.country,
|
||||
locale: account.locale,
|
||||
referralLink: account.referral_link,
|
||||
isPaired: account.is_paired,
|
||||
accountType: {
|
||||
".tag": account.account_type['.tag'],
|
||||
},
|
||||
rootInfo: {
|
||||
".tag": account.root_info['.tag'],
|
||||
rootNamespaceId: account.root_info.root_namespace_id,
|
||||
homeNamespaceId: account.root_info.home_namespace_id,
|
||||
},
|
||||
screenName: `${account.name.display_name} - ${account.email}`,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
13
packages/backend/src/apps/dropbox/common/add-auth-header.ts
Normal file
13
packages/backend/src/apps/dropbox/common/add-auth-header.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
requestConfig.headers['Content-Type'] = 'application/json';
|
||||
|
||||
if (!requestConfig.additionalProperties?.skipAddingAuthHeader && $.auth.data?.accessToken) {
|
||||
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,8 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const getCurrentAccount = async ($: IGlobalVariable): Promise<IJSONObject> => {
|
||||
const response = await $.http.post('/2/users/get_current_account', null);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export default getCurrentAccount;
|
8
packages/backend/src/apps/dropbox/common/scopes.ts
Normal file
8
packages/backend/src/apps/dropbox/common/scopes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const scopes = [
|
||||
'account_info.read',
|
||||
'files.metadata.read',
|
||||
'files.content.write',
|
||||
'files.content.read',
|
||||
];
|
||||
|
||||
export default scopes;
|
0
packages/backend/src/apps/dropbox/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/dropbox/index.d.ts
vendored
Normal file
18
packages/backend/src/apps/dropbox/index.ts
Normal file
18
packages/backend/src/apps/dropbox/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Dropbox',
|
||||
key: 'dropbox',
|
||||
iconUrl: '{BASE_URL}/apps/dropbox/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/dropbox/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://dropbox.com',
|
||||
apiBaseUrl: 'https://api.dropboxapi.com',
|
||||
primaryColor: '0061ff',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
actions,
|
||||
});
|
79
packages/backend/src/apps/filter/actions/continue/index.ts
Normal file
79
packages/backend/src/apps/filter/actions/continue/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
type TGroupItem = {
|
||||
key: string;
|
||||
operator: keyof TOperators;
|
||||
value: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type TGroup = Record<'and', TGroupItem[]>;
|
||||
|
||||
const isEqual = (a: string, b: string) => a === b;
|
||||
const isNotEqual = (a: string, b: string) => !isEqual(a, b)
|
||||
const isGreaterThan = (a: string, b: string) => Number(a) > Number(b);
|
||||
const isLessThan = (a: string, b: string) => Number(a) < Number(b);
|
||||
const isGreaterThanOrEqual = (a: string, b: string) => Number(a) >= Number(b);
|
||||
const isLessThanOrEqual = (a: string, b: string) => Number(a) <= Number(b);
|
||||
const contains = (a: string, b: string) => a.includes(b);
|
||||
const doesNotContain = (a: string, b: string) => !contains(a, b);
|
||||
|
||||
type TOperatorFunc = (a: string, b: string) => boolean;
|
||||
|
||||
type TOperators = {
|
||||
equal: TOperatorFunc;
|
||||
not_equal: TOperatorFunc;
|
||||
greater_than: TOperatorFunc;
|
||||
less_than: TOperatorFunc;
|
||||
greater_than_or_equal: TOperatorFunc;
|
||||
less_than_or_equal: TOperatorFunc;
|
||||
contains: TOperatorFunc;
|
||||
not_contains: TOperatorFunc;
|
||||
};
|
||||
|
||||
const operators: TOperators = {
|
||||
'equal': isEqual,
|
||||
'not_equal': isNotEqual,
|
||||
'greater_than': isGreaterThan,
|
||||
'less_than': isLessThan,
|
||||
'greater_than_or_equal': isGreaterThanOrEqual,
|
||||
'less_than_or_equal': isLessThanOrEqual,
|
||||
'contains': contains,
|
||||
'not_contains': doesNotContain,
|
||||
};
|
||||
|
||||
const operate = (operation: keyof TOperators, a: string, b: string) => {
|
||||
return operators[operation](a, b);
|
||||
};
|
||||
|
||||
export default defineAction({
|
||||
name: 'Continue if conditions match',
|
||||
key: 'continueIfMatches',
|
||||
description: 'Let the execution continue if the conditions match',
|
||||
arguments: [],
|
||||
|
||||
async run($) {
|
||||
const orGroups = $.step.parameters.or as TGroup[];
|
||||
|
||||
const matchingGroups = orGroups.reduce((groups, group) => {
|
||||
const matchingConditions = group.and
|
||||
.filter((condition) => operate(condition.operator, condition.key, condition.value));
|
||||
|
||||
if (matchingConditions.length) {
|
||||
return groups.concat([{ and: matchingConditions }]);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, []);
|
||||
|
||||
if (matchingGroups.length === 0) {
|
||||
$.execution.exit();
|
||||
}
|
||||
|
||||
$.setActionItem({
|
||||
raw: {
|
||||
or: matchingGroups,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/filter/actions/index.ts
Normal file
3
packages/backend/src/apps/filter/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import continueIfMatches from './continue';
|
||||
|
||||
export default [continueIfMatches];
|
8
packages/backend/src/apps/filter/assets/favicon.svg
Normal file
8
packages/backend/src/apps/filter/assets/favicon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="800px" height="800px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Shape" fill="#000000" transform="translate(42.666667, 85.333333)">
|
||||
<path d="M3.55271368e-14,1.42108547e-14 L191.565013,234.666667 L192,234.666667 L192,384 L234.666667,384 L234.666667,234.666667 L426.666667,1.42108547e-14 L3.55271368e-14,1.42108547e-14 Z M214.448,192 L211.81248,192 L89.9076267,42.6666667 L336.630187,42.6666667 L214.448,192 Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 628 B |
0
packages/backend/src/apps/filter/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/filter/index.d.ts
vendored
Normal file
14
packages/backend/src/apps/filter/index.ts
Normal file
14
packages/backend/src/apps/filter/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import actions from './actions';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Filter',
|
||||
key: 'filter',
|
||||
iconUrl: '{BASE_URL}/apps/filter/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/filter/connection',
|
||||
supportsConnections: false,
|
||||
baseUrl: '',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '001F52',
|
||||
actions,
|
||||
});
|
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
|
||||
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
|
||||
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
|
||||
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
|
||||
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
|
||||
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
|
||||
</svg>
|
After Width: | Height: | Size: 755 B |
@@ -0,0 +1,24 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import authScope from '../common/auth-scope';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
redirect_uri: redirectUri,
|
||||
prompt: 'select_account',
|
||||
scope: authScope.join(' '),
|
||||
response_type: 'code',
|
||||
access_type: 'offline',
|
||||
});
|
||||
|
||||
const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
48
packages/backend/src/apps/google-drive/auth/index.ts
Normal file
48
packages/backend/src/apps/google-drive/auth/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import refreshToken from './refresh-token';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/google-drive/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input a redirect URL in Google Cloud, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentUser from '../common/get-current-user';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
const currentUser = await getCurrentUser($);
|
||||
return !!currentUser.resourceName;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
26
packages/backend/src/apps/google-drive/auth/refresh-token.ts
Normal file
26
packages/backend/src/apps/google-drive/auth/refresh-token.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import authScope from '../common/auth-scope';
|
||||
|
||||
const refreshToken = async ($: IGlobalVariable) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: $.auth.data.refreshToken as string,
|
||||
});
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
params.toString()
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
expiresIn: data.expires_in,
|
||||
scope: authScope.join(' '),
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
@@ -0,0 +1,57 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentUser from '../common/get-current-user';
|
||||
|
||||
type TUser = {
|
||||
displayName: string;
|
||||
metadata: {
|
||||
primary: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type TEmailAddress = {
|
||||
value: string;
|
||||
metadata: {
|
||||
primary: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, {
|
||||
client_id: $.auth.data.clientId,
|
||||
client_secret: $.auth.data.clientSecret,
|
||||
code: $.auth.data.code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
|
||||
const currentUser = await getCurrentUser($);
|
||||
|
||||
const { displayName } = currentUser.names.find(
|
||||
(name: TUser) => name.metadata.primary
|
||||
);
|
||||
const { value: email } = currentUser.emailAddresses.find(
|
||||
(emailAddress: TEmailAddress) => emailAddress.metadata.primary
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
clientSecret: $.auth.data.clientSecret,
|
||||
scope: $.auth.data.scope,
|
||||
idToken: data.id_token,
|
||||
expiresIn: data.expires_in,
|
||||
refreshToken: data.refresh_token,
|
||||
resourceName: currentUser.resourceName,
|
||||
screenName: `${displayName} - ${email}`,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,11 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
if ($.auth.data?.accessToken) {
|
||||
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,7 @@
|
||||
const authScope: string[] = [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
];
|
||||
|
||||
export default authScope;
|
@@ -0,0 +1,10 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const getCurrentUser = async ($: IGlobalVariable) => {
|
||||
const { data: currentUser } = await $.http.get(
|
||||
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
|
||||
);
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
@@ -0,0 +1,4 @@
|
||||
import listFolders from './list-folders';
|
||||
import listDrives from './list-drives';
|
||||
|
||||
export default [listFolders, listDrives];
|
@@ -0,0 +1,35 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List drives',
|
||||
key: 'listDrives',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const drives: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [{ value: null, name: 'My Google Drive' }],
|
||||
};
|
||||
|
||||
const params = {
|
||||
pageSize: 100,
|
||||
pageToken: undefined as unknown as string,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/drives`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.drives) {
|
||||
for (const drive of data.drives) {
|
||||
drives.data.push({
|
||||
value: drive.id,
|
||||
name: drive.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
|
||||
return drives;
|
||||
},
|
||||
};
|
@@ -0,0 +1,40 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List Folders',
|
||||
key: 'listFolders',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const folders: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
const params = {
|
||||
q: `mimeType='application/vnd.google-apps.folder'`,
|
||||
orderBy: 'createdTime desc',
|
||||
pageToken: undefined as unknown as string,
|
||||
pageSize: 1000,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(
|
||||
`https://www.googleapis.com/drive/v3/files`,
|
||||
{
|
||||
params,
|
||||
}
|
||||
);
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
for (const file of data.files) {
|
||||
folders.data.push({
|
||||
value: file.id,
|
||||
name: file.name,
|
||||
});
|
||||
}
|
||||
} while (params.pageToken);
|
||||
|
||||
return folders;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/google-drive/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/google-drive/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/google-drive/index.ts
Normal file
20
packages/backend/src/apps/google-drive/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: 'Google Drive',
|
||||
key: 'google-drive',
|
||||
baseUrl: 'https://drive.google.com',
|
||||
apiBaseUrl: 'https://www.googleapis.com/drive',
|
||||
iconUrl: '{BASE_URL}/apps/google-drive/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/google-drive/connection',
|
||||
primaryColor: '1FA463',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
6
packages/backend/src/apps/google-drive/triggers/index.ts
Normal file
6
packages/backend/src/apps/google-drive/triggers/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import newFiles from './new-files';
|
||||
import newFilesInFolder from './new-files-in-folder';
|
||||
import newFolders from './new-folders';
|
||||
import updatedFiles from './updated-files';
|
||||
|
||||
export default [newFiles, newFilesInFolder, newFolders, updatedFiles];
|
@@ -0,0 +1,54 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import newFilesInFolder from './new-files-in-folder';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New Files in Folder',
|
||||
key: 'newFilesInFolder',
|
||||
pollInterval: 15,
|
||||
description:
|
||||
'Triggers when a new file is added directly to a specific folder (but not its subfolder).',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Drive',
|
||||
key: 'driveId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDrives',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Folder',
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'Check a specific folder for new files. Please note: new files added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await newFilesInFolder($);
|
||||
},
|
||||
});
|
@@ -0,0 +1,36 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newFilesInFolder = async ($: IGlobalVariable) => {
|
||||
let q = "mimeType!='application/vnd.google-apps.folder'";
|
||||
if ($.step.parameters.folderId) {
|
||||
q += ` and '${$.step.parameters.folderId}' in parents`;
|
||||
} else {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
const params = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.files?.length) {
|
||||
for (const file of data.files) {
|
||||
$.pushTriggerItem({
|
||||
raw: file,
|
||||
meta: {
|
||||
internalId: file.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
};
|
||||
|
||||
export default newFilesInFolder;
|
@@ -0,0 +1,34 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import newFiles from './new-files';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New Files',
|
||||
key: 'newFiles',
|
||||
pollInterval: 15,
|
||||
description: 'Triggers when any new file is added (inside of any folder).',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Drive',
|
||||
key: 'driveId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDrives',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await newFiles($);
|
||||
},
|
||||
});
|
@@ -0,0 +1,30 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newFiles = async ($: IGlobalVariable) => {
|
||||
const params = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q: `mimeType!='application/vnd.google-apps.folder'`,
|
||||
driveId: $.step.parameters.driveId,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get('/v3/files', { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.files?.length) {
|
||||
for (const file of data.files) {
|
||||
$.pushTriggerItem({
|
||||
raw: file,
|
||||
meta: {
|
||||
internalId: file.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
};
|
||||
|
||||
export default newFiles;
|
@@ -0,0 +1,54 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import newFolders from './new-folders';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New Folders',
|
||||
key: 'newFolders',
|
||||
pollInterval: 15,
|
||||
description:
|
||||
'Triggers when a new folder is added directly to a specific folder (but not its subfolder).',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Drive',
|
||||
key: 'driveId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDrives',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Folder',
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'Check a specific folder for new subfolders. Please note: new folders added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await newFolders($);
|
||||
},
|
||||
});
|
@@ -0,0 +1,37 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newFolders = async ($: IGlobalVariable) => {
|
||||
let q = "mimeType='application/vnd.google-apps.folder'";
|
||||
if ($.step.parameters.folderId) {
|
||||
q += ` and '${$.step.parameters.folderId}' in parents`;
|
||||
} else {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
|
||||
const params = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.files?.length) {
|
||||
for (const file of data.files) {
|
||||
$.pushTriggerItem({
|
||||
raw: file,
|
||||
meta: {
|
||||
internalId: file.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
};
|
||||
|
||||
export default newFolders;
|
@@ -0,0 +1,71 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import updatedFiles from './updated-files';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'Updated Files',
|
||||
key: 'updatedFiles',
|
||||
pollInterval: 15,
|
||||
description:
|
||||
'Triggers when a file is updated in a specific folder (but not its subfolder).',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Drive',
|
||||
key: 'driveId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDrives',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Folder',
|
||||
key: 'folderId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description:
|
||||
'Check a specific folder for updated files. Please note: files located in subfolders of the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.',
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listFolders',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Include Deleted',
|
||||
key: 'includeDeleted',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
value: true,
|
||||
description: 'Should this trigger also on files that are deleted?',
|
||||
options: [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await updatedFiles($);
|
||||
},
|
||||
});
|
@@ -0,0 +1,41 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const updatedFiles = async ($: IGlobalVariable) => {
|
||||
let q = `mimeType!='application/vnd.google-apps.folder'`;
|
||||
if ($.step.parameters.includeDeleted === false) {
|
||||
q += ` and trashed=${$.step.parameters.includeDeleted}`;
|
||||
}
|
||||
|
||||
if ($.step.parameters.folderId) {
|
||||
q += ` and '${$.step.parameters.folderId}' in parents`;
|
||||
} else {
|
||||
q += ` and parents in 'root'`;
|
||||
}
|
||||
|
||||
const params = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'modifiedTime desc',
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
q,
|
||||
driveId: $.step.parameters.driveId,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(`/v3/files`, { params });
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.files?.length) {
|
||||
for (const file of data.files) {
|
||||
$.pushTriggerItem({
|
||||
raw: file,
|
||||
meta: {
|
||||
internalId: `${file.id}-${file.modifiedTime}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
};
|
||||
|
||||
export default updatedFiles;
|
89
packages/backend/src/apps/google-sheets/assets/favicon.svg
Normal file
89
packages/backend/src/apps/google-sheets/assets/favicon.svg
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="49px" height="67px" viewBox="0 0 49 67" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
|
||||
<title>Sheets-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-1"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-3"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-5"></path>
|
||||
<linearGradient x1="50.0053945%" y1="8.58610612%" x2="50.0053945%" y2="100.013939%" id="linearGradient-7">
|
||||
<stop stop-color="#263238" stop-opacity="0.2" offset="0%"></stop>
|
||||
<stop stop-color="#263238" stop-opacity="0.02" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-8"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-10"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-12"></path>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="path-14"></path>
|
||||
<radialGradient cx="3.16804688%" cy="2.71744318%" fx="3.16804688%" fy="2.71744318%" r="161.248516%" gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" id="radialGradient-16">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%"></stop>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Consumer-Apps-Sheets-Large-VD-R8-" transform="translate(-451.000000, -451.000000)">
|
||||
<g id="Hero" transform="translate(0.000000, 63.000000)">
|
||||
<g id="Personal" transform="translate(277.000000, 299.000000)">
|
||||
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)">
|
||||
<g id="Group">
|
||||
<g id="Clipped">
|
||||
<mask id="mask-2" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" id="Path" fill="#0F9D58" fill-rule="nonzero" mask="url(#mask-2)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-4" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" id="Shape" fill="#F1F1F1" fill-rule="nonzero" mask="url(#mask-4)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-6" fill="white">
|
||||
<use xlink:href="#path-5"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<polygon id="Path" fill="url(#linearGradient-7)" fill-rule="nonzero" mask="url(#mask-6)" points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75"></polygon>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-9" fill="white">
|
||||
<use xlink:href="#path-8"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<g id="Group" mask="url(#mask-9)">
|
||||
<g transform="translate(26.625000, -2.958333)">
|
||||
<path d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" id="Path" fill="#87CEAC" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-11" fill="white">
|
||||
<use xlink:href="#path-10"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" id="Path" fill-opacity="0.2" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-11)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-13" fill="white">
|
||||
<use xlink:href="#path-12"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" id="Path" fill-opacity="0.2" fill="#263238" fill-rule="nonzero" mask="url(#mask-13)"></path>
|
||||
</g>
|
||||
<g id="Clipped">
|
||||
<mask id="mask-15" fill="white">
|
||||
<use xlink:href="#path-14"></use>
|
||||
</mask>
|
||||
<g id="SVGID_1_"></g>
|
||||
<path d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" id="Path" fill-opacity="0.1" fill="#263238" fill-rule="nonzero" mask="url(#mask-15)"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" id="Path" fill="url(#radialGradient-16)" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,24 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import authScope from '../common/auth-scope';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
redirect_uri: redirectUri,
|
||||
prompt: 'select_account',
|
||||
scope: authScope.join(' '),
|
||||
response_type: 'code',
|
||||
access_type: 'offline',
|
||||
});
|
||||
|
||||
const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
48
packages/backend/src/apps/google-sheets/auth/index.ts
Normal file
48
packages/backend/src/apps/google-sheets/auth/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import refreshToken from './refresh-token';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/google-sheets/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input a redirect URL in Google Cloud, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
refreshToken,
|
||||
};
|
@@ -0,0 +1,9 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentUser from '../common/get-current-user';
|
||||
|
||||
const isStillVerified = async ($: IGlobalVariable) => {
|
||||
const currentUser = await getCurrentUser($);
|
||||
return !!currentUser.resourceName;
|
||||
};
|
||||
|
||||
export default isStillVerified;
|
@@ -0,0 +1,26 @@
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
import authScope from '../common/auth-scope';
|
||||
|
||||
const refreshToken = async ($: IGlobalVariable) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: $.auth.data.refreshToken as string,
|
||||
});
|
||||
|
||||
const { data } = await $.http.post(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
params.toString()
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
expiresIn: data.expires_in,
|
||||
scope: authScope.join(' '),
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
};
|
||||
|
||||
export default refreshToken;
|
@@ -0,0 +1,57 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import getCurrentUser from '../common/get-current-user';
|
||||
|
||||
type TUser = {
|
||||
displayName: string;
|
||||
metadata: {
|
||||
primary: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type TEmailAddress = {
|
||||
value: string;
|
||||
metadata: {
|
||||
primary: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, {
|
||||
client_id: $.auth.data.clientId,
|
||||
client_secret: $.auth.data.clientSecret,
|
||||
code: $.auth.data.code,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: redirectUri,
|
||||
});
|
||||
|
||||
await $.auth.set({
|
||||
accessToken: data.access_token,
|
||||
tokenType: data.token_type,
|
||||
});
|
||||
|
||||
const currentUser = await getCurrentUser($);
|
||||
|
||||
const { displayName } = currentUser.names.find(
|
||||
(name: TUser) => name.metadata.primary
|
||||
);
|
||||
const { value: email } = currentUser.emailAddresses.find(
|
||||
(emailAddress: TEmailAddress) => emailAddress.metadata.primary
|
||||
);
|
||||
|
||||
await $.auth.set({
|
||||
clientId: $.auth.data.clientId,
|
||||
clientSecret: $.auth.data.clientSecret,
|
||||
scope: $.auth.data.scope,
|
||||
idToken: data.id_token,
|
||||
expiresIn: data.expires_in,
|
||||
refreshToken: data.refresh_token,
|
||||
resourceName: currentUser.resourceName,
|
||||
screenName: `${displayName} - ${email}`,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,11 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
if ($.auth.data?.accessToken) {
|
||||
requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,8 @@
|
||||
const authScope: string[] = [
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/spreadsheets',
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
];
|
||||
|
||||
export default authScope;
|
@@ -0,0 +1,10 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const getCurrentUser = async ($: IGlobalVariable) => {
|
||||
const { data: currentUser } = await $.http.get(
|
||||
'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses'
|
||||
);
|
||||
return currentUser;
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
@@ -0,0 +1,3 @@
|
||||
import listDrives from './list-drives';
|
||||
|
||||
export default [listDrives];
|
@@ -0,0 +1,38 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List drives',
|
||||
key: 'listDrives',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
const drives: {
|
||||
data: IJSONObject[];
|
||||
} = {
|
||||
data: [{ value: null, name: 'My Google Drive' }],
|
||||
};
|
||||
|
||||
const params = {
|
||||
pageSize: 100,
|
||||
pageToken: undefined as unknown as string,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(
|
||||
`https://www.googleapis.com/drive/v3/drives`,
|
||||
{ params }
|
||||
);
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.drives) {
|
||||
for (const drive of data.drives) {
|
||||
drives.data.push({
|
||||
value: drive.id,
|
||||
name: drive.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
|
||||
return drives;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/google-sheets/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/google-sheets/index.d.ts
vendored
Normal file
20
packages/backend/src/apps/google-sheets/index.ts
Normal file
20
packages/backend/src/apps/google-sheets/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: 'Google Sheets',
|
||||
key: 'google-sheets',
|
||||
baseUrl: 'https://docs.google.com/spreadsheets',
|
||||
apiBaseUrl: 'https://sheets.googleapis.com',
|
||||
iconUrl: '{BASE_URL}/apps/google-sheets/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/google-sheets/connection',
|
||||
primaryColor: '0F9D58',
|
||||
supportsConnections: true,
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
dynamicData,
|
||||
});
|
@@ -0,0 +1,3 @@
|
||||
import newSpreadsheets from './new-spreadsheets';
|
||||
|
||||
export default [newSpreadsheets];
|
@@ -0,0 +1,33 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import newSpreadsheets from './new-spreadsheets'
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'New Spreadsheets',
|
||||
key: 'newSpreadsheets',
|
||||
pollInterval: 15,
|
||||
description: 'Triggers when you create a new spreadsheet.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'Drive',
|
||||
key: 'driveId',
|
||||
type: 'dropdown' as const,
|
||||
required: false,
|
||||
description: 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
|
||||
variables: false,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listDrives',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await newSpreadsheets($);
|
||||
},
|
||||
});
|
@@ -0,0 +1,33 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
const newSpreadsheets = async ($: IGlobalVariable) => {
|
||||
const params = {
|
||||
pageToken: undefined as unknown as string,
|
||||
orderBy: 'createdTime desc',
|
||||
q: `mimeType='application/vnd.google-apps.spreadsheet'`,
|
||||
fields: '*',
|
||||
pageSize: 1000,
|
||||
driveId: $.step.parameters.driveId,
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get(
|
||||
'https://www.googleapis.com/drive/v3/files',
|
||||
{ params }
|
||||
);
|
||||
params.pageToken = data.nextPageToken;
|
||||
|
||||
if (data.files?.length) {
|
||||
for (const file of data.files) {
|
||||
$.pushTriggerItem({
|
||||
raw: file,
|
||||
meta: {
|
||||
internalId: file.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} while (params.pageToken);
|
||||
};
|
||||
|
||||
export default newSpreadsheets;
|
@@ -2,6 +2,13 @@ import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
type TMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
||||
|
||||
type THeaderEntry = {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type THeaderEntries = THeaderEntry[];
|
||||
|
||||
export default defineAction({
|
||||
name: 'Custom Request',
|
||||
key: 'customRequest',
|
||||
@@ -38,15 +45,47 @@ export default defineAction({
|
||||
description: 'Place raw JSON data here.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Headers',
|
||||
key: 'headers',
|
||||
type: 'dynamic' as const,
|
||||
required: false,
|
||||
description: 'Add or remove headers as needed',
|
||||
value: [{
|
||||
key: 'Content-Type',
|
||||
value: 'application/json'
|
||||
}],
|
||||
fields: [
|
||||
{
|
||||
label: 'Key',
|
||||
key: 'key',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Header key',
|
||||
variables: false,
|
||||
},
|
||||
{
|
||||
label: 'Value',
|
||||
key: 'value',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Header value',
|
||||
variables: true,
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const method = $.step.parameters.method as TMethod;
|
||||
const data = $.step.parameters.data as string;
|
||||
const url = $.step.parameters.url as string;
|
||||
const headers = $.step.parameters.headers as THeaderEntries;
|
||||
const maxFileSize = 25 * 1024 * 1024; // 25MB
|
||||
|
||||
const metadataResponse = await $.http.head(url);
|
||||
const headersObject = headers.reduce((result, entry) => ({ ...result, [entry.key]: entry.value }), {})
|
||||
|
||||
const metadataResponse = await $.http.head(url, { headers: headersObject });
|
||||
|
||||
if (Number(metadataResponse.headers['content-length']) > maxFileSize) {
|
||||
throw new Error(
|
||||
@@ -58,9 +97,7 @@ export default defineAction({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: headersObject,
|
||||
});
|
||||
|
||||
let responseData = response.data;
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
if ($.auth.data.apiBaseUrl) {
|
||||
requestConfig.baseURL = $.auth.data.apiBaseUrl as string;
|
||||
if ($.auth.data.serverUrl) {
|
||||
requestConfig.baseURL = $.auth.data.serverUrl as string;
|
||||
}
|
||||
|
||||
if ($.auth.data?.username && $.auth.data?.password) {
|
||||
|
3
packages/backend/src/apps/signalwire/actions/index.ts
Normal file
3
packages/backend/src/apps/signalwire/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import sendSms from './send-sms';
|
||||
|
||||
export default [sendSms];
|
@@ -0,0 +1,63 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Send an SMS',
|
||||
key: 'sendSms',
|
||||
description: 'Sends an SMS',
|
||||
arguments: [
|
||||
{
|
||||
label: 'From Number',
|
||||
key: 'fromNumber',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The number to send the SMS from. Include only country code. Example: 491234567890',
|
||||
variables: true,
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listIncomingPhoneNumbers',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'To Number',
|
||||
key: 'toNumber',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description:
|
||||
'The number to send the SMS to. Include only country code. Example: 491234567890',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
key: 'message',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'The content of the message.',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages`;
|
||||
|
||||
const Body = $.step.parameters.message;
|
||||
const From = $.step.parameters.fromNumber;
|
||||
const To = '+' + ($.step.parameters.toNumber as string).trim();
|
||||
|
||||
const response = await $.http.post(requestPath, null, {
|
||||
params: {
|
||||
Body,
|
||||
From,
|
||||
To,
|
||||
}
|
||||
});
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
1
packages/backend/src/apps/signalwire/assets/favicon.svg
Normal file
1
packages/backend/src/apps/signalwire/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="a1100050-5390-497e-a7fa-2bb69ec95c7c" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 318.33 362.7"><defs><style>.a463a3b0-e95c-44d7-b809-5997966784eb{fill:#044ef4;}.b59fade4-5c2c-49f1-a6bf-917d1f195a19{fill:#f72a72;}</style></defs><path class="a463a3b0-e95c-44d7-b809-5997966784eb" d="M389.17,278c0,10.31-2.8,17.06-8.37,22.62q-50.07,49.95-100.19,99.85C269,412,252.06,412.54,240.55,402c-12.12-11.13-12.6-29.39-.75-41.47,15-15.34,30.32-30.47,45.56-45.63q27.47-27.3,55.06-54.45c8.91-8.75,20.84-11.07,31.77-6.1C383.3,259.36,388.79,268.25,389.17,278Z" transform="translate(-70.83 -46.81)"/><path class="a463a3b0-e95c-44d7-b809-5997966784eb" d="M70.84,172.94c.16-5.21,2.93-11.81,8.36-17.24q49.89-49.77,99.8-99.53c6.92-6.91,15.08-10.45,24.83-9.06,11.55,1.65,19.62,8.12,23.38,19.22s1.16,21.14-7,29.43q-23.87,24.21-48,48.1-26.22,26.07-52.58,52c-8.93,8.76-20.84,10.92-31.8,6.13C77.17,197.35,70.65,187.14,70.84,172.94Z" transform="translate(-70.83 -46.81)"/><path class="b59fade4-5c2c-49f1-a6bf-917d1f195a19" d="M93.68,210.69c3.79-.17,6.91-.08,10-.49a34.39,34.39,0,0,0,20.56-10.34c6.38-6.52,12.79-13,19.33-19.66,1.23,1.09,2,1.7,2.66,2.38q36.92,36.9,73.81,73.83c8.07,8.1,10.9,17.87,7.66,28.86-3.12,10.58-10.39,17.35-21.17,19.77-9.33,2.1-18.23.31-25.07-6.47C152.25,269.64,123.32,240.42,93.68,210.69Z" transform="translate(-70.83 -46.81)"/><path class="b59fade4-5c2c-49f1-a6bf-917d1f195a19" d="M366.57,246c-15-1.53-25.7,4.26-34.72,14.22-4.89,5.4-10.23,10.39-15.51,15.69-1.1-1-1.86-1.59-2.56-2.28q-36.94-36.93-73.86-73.89c-8.07-8.1-10.89-17.89-7.56-28.89,3.19-10.56,10.47-17.31,21.28-19.68,9.56-2.1,18.45,0,25.44,6.92q43,42.57,85.61,85.47C365.12,244,365.44,244.54,366.57,246Z" transform="translate(-70.83 -46.81)"/></svg>
|
After Width: | Height: | Size: 1.7 KiB |
65
packages/backend/src/apps/signalwire/auth/index.ts
Normal file
65
packages/backend/src/apps/signalwire/auth/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'accountSid',
|
||||
label: 'Project ID',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description:
|
||||
'Log into your SignalWire account and find the Project ID',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'authToken',
|
||||
label: 'API Token',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'API Token in the respective project',
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'spaceRegion',
|
||||
label: 'SignalWire Region',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: '',
|
||||
placeholder: null,
|
||||
description: 'Most people should choose the default, "US"',
|
||||
clickToCopy: false,
|
||||
options: [
|
||||
{
|
||||
label: 'US',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: 'EU',
|
||||
value: 'eu-',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'spaceName',
|
||||
label: 'Space Name',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: 'Name of your SignalWire space that contains the project',
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
@@ -0,0 +1,10 @@
|
||||
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(`/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}`);
|
||||
|
||||
await $.auth.set({
|
||||
screenName: `${data.friendly_name} (${$.auth.data.accountSid})`,
|
||||
});
|
||||
};
|
||||
|
||||
export default verifyCredentials;
|
@@ -0,0 +1,27 @@
|
||||
import { TBeforeRequest } from '@automatisch/types';
|
||||
|
||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||
const authData = $.auth.data || {};
|
||||
|
||||
requestConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
|
||||
if (
|
||||
authData.accountSid &&
|
||||
authData.authToken
|
||||
) {
|
||||
requestConfig.auth = {
|
||||
username: authData.accountSid as string,
|
||||
password: authData.authToken as string,
|
||||
};
|
||||
}
|
||||
|
||||
if (authData.spaceName) {
|
||||
const serverUrl = `https://${authData.spaceName}.${authData.spaceRegion}signalwire.com`;
|
||||
|
||||
requestConfig.baseURL = serverUrl as string;
|
||||
}
|
||||
|
||||
return requestConfig;
|
||||
};
|
||||
|
||||
export default addAuthHeader;
|
@@ -0,0 +1,3 @@
|
||||
import listIncomingPhoneNumbers from './list-incoming-phone-numbers';
|
||||
|
||||
export default [listIncomingPhoneNumbers];
|
@@ -0,0 +1,57 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
type TAggregatedResponse = {
|
||||
data: IJSONObject[];
|
||||
error?: IJSONObject;
|
||||
};
|
||||
|
||||
type TResponse = {
|
||||
incoming_phone_numbers: TIncomingPhoneNumber[];
|
||||
next_page_uri: string;
|
||||
};
|
||||
|
||||
type TIncomingPhoneNumber = {
|
||||
capabilities: {
|
||||
sms: boolean;
|
||||
};
|
||||
sid: string;
|
||||
friendly_name: string;
|
||||
phone_number: string;
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'List incoming phone numbers',
|
||||
key: 'listIncomingPhoneNumbers',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers`;
|
||||
|
||||
const aggregatedResponse: TAggregatedResponse = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
do {
|
||||
const { data } = await $.http.get<TResponse>(requestPath);
|
||||
|
||||
const smsCapableIncomingPhoneNumbers = data.incoming_phone_numbers
|
||||
.filter((incomingPhoneNumber) => {
|
||||
return incomingPhoneNumber.capabilities.sms;
|
||||
})
|
||||
.map((incomingPhoneNumber) => {
|
||||
const friendlyName = incomingPhoneNumber.friendly_name;
|
||||
const phoneNumber = incomingPhoneNumber.phone_number;
|
||||
const name = [friendlyName, phoneNumber].filter(Boolean).join(' - ');
|
||||
|
||||
return {
|
||||
value: phoneNumber,
|
||||
name,
|
||||
};
|
||||
})
|
||||
aggregatedResponse.data.push(...smsCapableIncomingPhoneNumbers)
|
||||
|
||||
requestPath = data.next_page_uri;
|
||||
} while (requestPath);
|
||||
|
||||
return aggregatedResponse;
|
||||
},
|
||||
};
|
0
packages/backend/src/apps/signalwire/index.d.ts
vendored
Normal file
0
packages/backend/src/apps/signalwire/index.d.ts
vendored
Normal file
22
packages/backend/src/apps/signalwire/index.ts
Normal file
22
packages/backend/src/apps/signalwire/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import defineApp from '../../helpers/define-app';
|
||||
import addAuthHeader from './common/add-auth-header';
|
||||
import auth from './auth';
|
||||
import triggers from './triggers';
|
||||
import actions from './actions';
|
||||
import dynamicData from './dynamic-data';
|
||||
|
||||
export default defineApp({
|
||||
name: 'SignalWire',
|
||||
key: 'signalwire',
|
||||
iconUrl: '{BASE_URL}/apps/signalwire/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/apps/signalwire/connection',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://signalwire.com',
|
||||
apiBaseUrl: '',
|
||||
primaryColor: '044cf6',
|
||||
beforeRequest: [addAuthHeader],
|
||||
auth,
|
||||
triggers,
|
||||
actions,
|
||||
dynamicData,
|
||||
});
|
3
packages/backend/src/apps/signalwire/triggers/index.ts
Normal file
3
packages/backend/src/apps/signalwire/triggers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import receiveSms from './receive-sms';
|
||||
|
||||
export default [receiveSms];
|
@@ -0,0 +1,27 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
const fetchMessages = async ($: IGlobalVariable) => {
|
||||
const toNumber = $.step.parameters.toNumber as string;
|
||||
|
||||
let response;
|
||||
let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages?To=${toNumber}`;
|
||||
|
||||
do {
|
||||
response = await $.http.get(requestPath);
|
||||
|
||||
response.data.messages.forEach((message: IJSONObject) => {
|
||||
const dataItem = {
|
||||
raw: message,
|
||||
meta: {
|
||||
internalId: message.date_sent as string,
|
||||
},
|
||||
};
|
||||
|
||||
$.pushTriggerItem(dataItem);
|
||||
});
|
||||
|
||||
requestPath = response.data.next_page_uri;
|
||||
} while (requestPath);
|
||||
};
|
||||
|
||||
export default fetchMessages;
|
@@ -0,0 +1,33 @@
|
||||
import defineTrigger from '../../../../helpers/define-trigger';
|
||||
import fetchMessages from './fetch-messages';
|
||||
|
||||
export default defineTrigger({
|
||||
name: 'Receive SMS',
|
||||
key: 'receiveSms',
|
||||
pollInterval: 15,
|
||||
description: 'Triggers when a new SMS is received.',
|
||||
arguments: [
|
||||
{
|
||||
label: 'To Number',
|
||||
key: 'toNumber',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
description:
|
||||
'The number to receive the SMS on. It should be a SignalWire number in your project.',
|
||||
source: {
|
||||
type: 'query',
|
||||
name: 'getDynamicData',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listIncomingPhoneNumbers',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
await fetchMessages($);
|
||||
},
|
||||
});
|
@@ -51,25 +51,20 @@ export default defineAction({
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Bot name',
|
||||
key: 'botName',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
value: 'Automatisch',
|
||||
description:
|
||||
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Bot icon',
|
||||
key: 'botIcon',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
description:
|
||||
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
||||
variables: true,
|
||||
additionalFields: {
|
||||
type: 'query',
|
||||
name: 'getDynamicFields',
|
||||
arguments: [
|
||||
{
|
||||
name: 'key',
|
||||
value: 'listFieldsAfterSendAsBot',
|
||||
},
|
||||
{
|
||||
name: 'parameters.sendAsBot',
|
||||
value: '{parameters.sendAsBot}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
|
@@ -29,6 +29,7 @@ const userScopes = [
|
||||
'groups:history',
|
||||
'groups:read',
|
||||
'groups:write',
|
||||
'im:read',
|
||||
'im:write',
|
||||
'mpim:write',
|
||||
'reactions:read',
|
||||
|
@@ -1,5 +1,24 @@
|
||||
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
|
||||
|
||||
type TChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TConversationListResponseData = {
|
||||
channels: TChannel[],
|
||||
response_metadata?: {
|
||||
next_cursor: string
|
||||
};
|
||||
needed?: string;
|
||||
error?: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
type TResponse = {
|
||||
data: TConversationListResponseData;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'List channels',
|
||||
key: 'listChannels',
|
||||
@@ -13,24 +32,33 @@ export default {
|
||||
error: null,
|
||||
};
|
||||
|
||||
const response = await $.http.get('/conversations.list', {
|
||||
params: {
|
||||
types: 'public_channel,private_channel',
|
||||
limit: 1000,
|
||||
exclude_archived: true,
|
||||
let nextCursor;
|
||||
do {
|
||||
const response: TResponse = await $.http.get('/conversations.list', {
|
||||
params: {
|
||||
types: 'public_channel,private_channel,im',
|
||||
cursor: nextCursor,
|
||||
limit: 1000,
|
||||
}
|
||||
});
|
||||
|
||||
nextCursor = response.data.response_metadata?.next_cursor;
|
||||
|
||||
if (response.data.error === 'missing_scope') {
|
||||
throw new Error(`Missing "${response.data.needed}" scope while authorizing. Please, reconnect your connection!`);
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.ok === false) {
|
||||
throw new Error(response.data);
|
||||
}
|
||||
if (response.data.ok === false) {
|
||||
throw new Error(JSON.stringify(response.data, null, 2));
|
||||
}
|
||||
|
||||
channels.data = response.data.channels.map((channel: IJSONObject) => {
|
||||
return {
|
||||
value: channel.id,
|
||||
name: channel.name,
|
||||
};
|
||||
});
|
||||
for (const channel of response.data.channels) {
|
||||
channels.data.push({
|
||||
value: channel.id as string,
|
||||
name: channel.name as string,
|
||||
});
|
||||
}
|
||||
} while (nextCursor);
|
||||
|
||||
return channels;
|
||||
},
|
||||
|
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
3
packages/backend/src/apps/slack/dynamic-fields/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import listFieldsAfterSendAsBot from './send-as-bot';
|
||||
|
||||
export default [listFieldsAfterSendAsBot];
|
@@ -0,0 +1,32 @@
|
||||
import { IGlobalVariable } from '@automatisch/types';
|
||||
|
||||
export default {
|
||||
name: 'List fields after send as bot',
|
||||
key: 'listFieldsAfterSendAsBot',
|
||||
|
||||
async run($: IGlobalVariable) {
|
||||
if ($.step.parameters.sendAsBot) {
|
||||
return [
|
||||
{
|
||||
label: 'Bot name',
|
||||
key: 'botName',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
value: 'Automatisch',
|
||||
description:
|
||||
'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Bot icon',
|
||||
key: 'botIcon',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
description:
|
||||
'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
|
||||
variables: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
@@ -3,6 +3,7 @@ import addAuthHeader from './common/add-auth-header';
|
||||
import actions from './actions';
|
||||
import auth from './auth';
|
||||
import dynamicData from './dynamic-data';
|
||||
import dynamicFields from './dynamic-fields';
|
||||
|
||||
export default defineApp({
|
||||
name: 'Slack',
|
||||
@@ -17,4 +18,5 @@ export default defineApp({
|
||||
auth,
|
||||
actions,
|
||||
dynamicData,
|
||||
dynamicFields,
|
||||
});
|
||||
|
@@ -0,0 +1,57 @@
|
||||
import defineAction from '../../../../helpers/define-action';
|
||||
|
||||
export default defineAction({
|
||||
name: 'Create playlist',
|
||||
key: 'createPlaylist',
|
||||
description: `Create playlist on user's account.`,
|
||||
arguments: [
|
||||
{
|
||||
label: 'Playlist name',
|
||||
key: 'playlistName',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
description: 'Playlist name',
|
||||
variables: true,
|
||||
},
|
||||
{
|
||||
label: 'Playlist visibility',
|
||||
key: 'playlistVisibility',
|
||||
type: 'dropdown' as const,
|
||||
required: true,
|
||||
description: 'Playlist visibility',
|
||||
variables: true,
|
||||
options: [
|
||||
{ label: 'public', value: 'Public' },
|
||||
{ label: 'private', value: 'Private' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Playlist description',
|
||||
key: 'playlistDescription',
|
||||
type: 'string' as const,
|
||||
required: false,
|
||||
description: 'Playlist description',
|
||||
variables: true,
|
||||
},
|
||||
],
|
||||
|
||||
async run($) {
|
||||
const playlistName = $.step.parameters.playlistName as string;
|
||||
const playlistDescription = $.step.parameters.playlistDescription as string;
|
||||
const playlistVisibility =
|
||||
$.step.parameters.playlistVisibility === 'public'
|
||||
? true
|
||||
: (false as boolean);
|
||||
|
||||
const response = await $.http.post(
|
||||
`v1/users/${$.auth.data.userId}/playlists`,
|
||||
{
|
||||
name: playlistName,
|
||||
public: playlistVisibility,
|
||||
description: playlistDescription,
|
||||
}
|
||||
);
|
||||
|
||||
$.setActionItem({ raw: response.data });
|
||||
},
|
||||
});
|
3
packages/backend/src/apps/spotify/actions/index.ts
Normal file
3
packages/backend/src/apps/spotify/actions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import cratePlaylist from './create-playlist';
|
||||
|
||||
export default [cratePlaylist];
|
6
packages/backend/src/apps/spotify/assets/favicon.svg
Normal file
6
packages/backend/src/apps/spotify/assets/favicon.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 256 256" version="1.1" preserveAspectRatio="xMidYMid">
|
||||
<title>Spotify</title>
|
||||
<g>
|
||||
<path d="M127.999236,0 C57.3087105,0 0,57.3085507 0,128.000764 C0,198.696035 57.3087105,256 127.999236,256 C198.697403,256 256,198.696035 256,128.000764 C256,57.3131363 198.697403,0.00611405337 127.997707,0.00611405337 L127.999236,0 Z M186.69886,184.613841 C184.406145,188.373984 179.48445,189.566225 175.724397,187.258169 C145.671485,168.900724 107.838626,164.743168 63.2835265,174.923067 C58.990035,175.901315 54.7102999,173.211132 53.7320747,168.916009 C52.7492641,164.620887 55.428684,160.34105 59.7328748,159.362801 C108.491286,148.222996 150.314998,153.019471 184.054595,173.639116 C187.814648,175.947171 189.00686,180.853699 186.69886,184.613841 L186.69886,184.613841 Z M202.365748,149.76068 C199.476927,154.456273 193.33245,155.938931 188.640026,153.050041 C154.234012,131.90153 101.787386,125.776777 61.0916907,138.130222 C55.8138602,139.724462 50.2395052,136.749975 48.6376614,131.481189 C47.0480455,126.203233 50.0239899,120.639444 55.2926496,119.034505 C101.778216,104.929384 159.568396,111.761839 199.079523,136.042273 C203.771946,138.931163 205.254569,145.075787 202.365748,149.762209 L202.365748,149.76068 Z M203.710807,113.467659 C162.457218,88.964062 94.394144,86.7110334 55.0068244,98.6655362 C48.6819873,100.58382 41.9933726,97.0132133 40.0766627,90.6882251 C38.1599527,84.3601798 41.7274177,77.675991 48.0568402,75.7531212 C93.2707135,62.0270714 168.433562,64.6790421 215.929451,92.8755277 C221.63067,96.2520136 223.495412,103.599577 220.117478,109.281061 C216.754829,114.970188 209.38757,116.845674 203.716921,113.467659 L203.710807,113.467659 Z" fill="#1ED760"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
27
packages/backend/src/apps/spotify/auth/generate-auth-url.ts
Normal file
27
packages/backend/src/apps/spotify/auth/generate-auth-url.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IField, IGlobalVariable } from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import scopes from '../common/scopes';
|
||||
|
||||
export default async function generateAuthUrl($: IGlobalVariable) {
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const redirectUri = oauthRedirectUrlField.value as string;
|
||||
const state = Math.random().toString() as string;
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: $.auth.data.clientId as string,
|
||||
client_secret: $.auth.data.clientSecret as string,
|
||||
grant_type: 'client_credentials',
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
scope: scopes.join(','),
|
||||
state: state,
|
||||
});
|
||||
|
||||
const url = `https://accounts.spotify.com/authorize?${searchParams}`;
|
||||
|
||||
await $.auth.set({
|
||||
url,
|
||||
});
|
||||
}
|
47
packages/backend/src/apps/spotify/auth/index.ts
Normal file
47
packages/backend/src/apps/spotify/auth/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import generateAuthUrl from './generate-auth-url';
|
||||
import verifyCredentials from './verify-credentials';
|
||||
import isStillVerified from './is-still-verified';
|
||||
import refreshToken from './refresh-token';
|
||||
|
||||
export default {
|
||||
fields: [
|
||||
{
|
||||
key: 'oAuthRedirectUrl',
|
||||
label: 'OAuth Redirect URL',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: true,
|
||||
value: '{WEB_APP_URL}/app/spotify/connections/add',
|
||||
placeholder: null,
|
||||
description:
|
||||
'When asked to input an OAuth callback or redirect URL in Spotify OAuth, enter the URL above.',
|
||||
clickToCopy: true,
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client Id',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
readOnly: false,
|
||||
value: null,
|
||||
placeholder: null,
|
||||
description: null,
|
||||
clickToCopy: false,
|
||||
},
|
||||
],
|
||||
refreshToken,
|
||||
generateAuthUrl,
|
||||
verifyCredentials,
|
||||
isStillVerified,
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user