Compare commits

...

376 Commits

Author SHA1 Message Date
Ali BARIN
b347df38b5 Release v0.1.5 2022-10-09 10:17:33 +02:00
Ali BARIN
5e2b8c6a1e chore: update cli version with 0.1.5 in Dockerfiles 2022-10-09 10:12:24 +02:00
Ömer Faruk Aydın
7b3352ba03 Merge pull request #581 from automatisch/docs/broken-links
docs: Fix broken links
2022-10-09 11:00:12 +03:00
Faruk AYDIN
0cfb51dfb1 docs: Add TBD to create flow page 2022-10-09 10:54:58 +03:00
Faruk AYDIN
84558af121 docs: Fix github issues link for the installation page 2022-10-09 10:52:43 +03:00
Ali BARIN
48fd4a0dbe feat: remove execution step data 2022-10-09 09:49:53 +02:00
Ömer Faruk Aydın
21c16d7272 Merge pull request #579 from automatisch/docs/hide-contribution-guide
docs: Hide contribution section from readme until it's finalised
2022-10-09 10:46:44 +03:00
Faruk AYDIN
f604e28ba4 docs: Hide contribution section from readme until it's finalised 2022-10-09 10:43:50 +03:00
dependabot[bot]
332a4a31f4 chore(deps): bump async from 2.6.3 to 2.6.4 in /packages/web
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-09 00:54:23 +02:00
Ömer Faruk Aydın
8869802d4f Merge pull request #577 from automatisch/e2e-tests
Introduce e2e tests
2022-10-09 01:48:17 +03:00
Ali BARIN
b0034de0a5 chore(e2e-tests): remove redundant configuration 2022-10-09 00:18:01 +02:00
Ali BARIN
c76491a8b0 chore: ignore cypress screenshots 2022-10-09 00:00:35 +02:00
Ali BARIN
ff2edeea17 chore(e2e-tests): rename cypress:open with open 2022-10-09 00:00:35 +02:00
Ali BARIN
284738ae7d test: list executions 2022-10-09 00:00:35 +02:00
Ali BARIN
b1459cbf3b test: display execution 2022-10-09 00:00:35 +02:00
Ali BARIN
5838216b72 test: create slack connection 2022-10-09 00:00:35 +02:00
Ali BARIN
ce40f7d474 test: list apps 2022-10-09 00:00:35 +02:00
Ali BARIN
f0d3db6f9a test: create flow 2022-10-09 00:00:35 +02:00
Ali BARIN
e47c54b336 chore: add major data-test attributes 2022-10-09 00:00:35 +02:00
Ali BARIN
38bcf6ada0 chore: add e2e-tests package 2022-10-09 00:00:35 +02:00
Ömer Faruk Aydın
8d92ba6083 Merge pull request #576 from automatisch/fix/use-cloned-step
fix: Do not use mutated step for global variables of actions
2022-10-08 21:24:54 +03:00
Faruk AYDIN
c0aa2e4f6c fix: Do not use mutated step for global variables of actions 2022-10-08 21:04:08 +03:00
dependabot[bot]
3705391ecc chore(deps): bump ejs from 3.1.6 to 3.1.8 in /packages/web
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.8)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-08 15:04:12 +02:00
dependabot[bot]
f5f8632678 chore(deps): bump ejs from 3.1.6 to 3.1.7 in /packages/cli
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-08 15:03:05 +02:00
Ömer Faruk Aydın
a36b805b7c Merge pull request #575 from automatisch/release/0.1.4
Release v.1.4
2022-10-08 15:42:56 +03:00
Ali BARIN
43cd8859bc Release v0.1.4 2022-10-08 14:38:27 +02:00
Ali BARIN
37548e6c39 chore: upgrade cli version with 0.1.4 in Dockerfiles 2022-10-08 14:37:55 +02:00
Ali BARIN
47930eca10 fix(backend): make getInterval optional 2022-10-08 14:36:38 +02:00
Ali BARIN
1b405728c4 Release v0.1.3 2022-10-08 13:03:31 +02:00
Ali BARIN
5d7bf38d13 chore: upgrade vers in Dockerfiles 2022-10-08 13:03:31 +02:00
Ali BARIN
2c2c1e8d89 chore: add 0.1.2 version in Dockerfiles 2022-10-08 12:57:27 +02:00
Ali BARIN
7f7ec6c115 docs: remove unnecessary text in READMEs 2022-10-08 12:56:29 +02:00
Ali BARIN
c9061b24a0 chore(backend): remove dist folder before building 2022-10-08 12:38:41 +02:00
Ali BARIN
a3dc7d2cde Release v0.1.1 2022-10-08 12:34:50 +02:00
Ömer Faruk Aydın
cf90e7cfd6 Merge pull request #568 from automatisch/revise-scheduler-outputs
fix: correct scheduler outputs
2022-10-08 12:21:17 +03:00
Ali BARIN
bbc9ea9571 fix: correct scheduler outputs 2022-10-08 10:37:26 +02:00
Ali BARIN
577abde2e9 fix: make auth in global variable optional 2022-10-08 10:36:48 +02:00
Ömer Faruk Aydın
8e972fd875 Merge pull request #566 from automatisch/fix/dynamic-import-fix
fix: dynamic import statements for get app
2022-10-07 19:21:41 +03:00
Faruk AYDIN
c7b290a380 fix: dynamic import statements for get app 2022-10-07 19:18:56 +03:00
Faruk AYDIN
3d14208175 chore: Remove local registry from docker files 2022-10-07 17:51:30 +02:00
Ömer Faruk Aydın
04ad03cfd9 Merge pull request #562 from automatisch/release/0.1.0
Release v0.1.0
2022-10-07 18:44:48 +03:00
Ali BARIN
48e9053124 chore: mark root package.json as private 2022-10-07 17:39:44 +02:00
Ali BARIN
bd5aae3469 Release v0.1.0 2022-10-07 17:32:56 +02:00
Ali BARIN
2d3e307189 chore: mark packages as public 2022-10-07 17:32:43 +02:00
Ali BARIN
c79c82cac7 docs: add description in packages 2022-10-07 17:22:55 +02:00
Ali BARIN
a80cd106bf chore: remove package local registry 2022-10-07 17:20:42 +02:00
Ali BARIN
e1d953c855 chore: reset versions to 0.0.0 2022-10-07 17:04:22 +02:00
Ömer Faruk Aydın
096837bf22 Merge pull request #561 from automatisch/chore/add-license-to-package-json-files
chore: Add AGPL-3.0 license to package.json files
2022-10-07 17:27:21 +03:00
Faruk AYDIN
ac4be28a03 chore: Add AGPL-3.0 license to package.json files 2022-10-07 17:10:14 +03:00
Ömer Faruk Aydın
42ffd907ae Merge pull request #557 from automatisch/refactor/redesign-app-architecture
refactor: Use functional design for the apps
2022-10-07 16:09:52 +03:00
Faruk AYDIN
11ef2ac4bc fix: Integration and auth problems wrt slack and twitter 2022-10-07 16:06:10 +03:00
Faruk AYDIN
2862953136 refactor: Redesign list channels for the slack actions 2022-10-07 16:06:10 +03:00
Ali BARIN
c958abdfcf fix(web): correct types 2022-10-07 16:06:10 +03:00
Faruk AYDIN
050f392dd1 feat: Add error management for new follower of me trigger 2022-10-07 16:06:10 +03:00
Faruk AYDIN
80b25694b5 feat: Implement error management for get user tweets 2022-10-07 16:06:10 +03:00
Faruk AYDIN
e462d1f7ea fix: Make generate request method generic for all requests of twitter 2022-10-07 16:06:10 +03:00
Ali BARIN
e7b47f5c98 refactor: use functional app implementation 2022-10-07 16:06:10 +03:00
Ali BARIN
89d7359060 fix(IAuth): add missing func signatures 2022-10-07 16:06:10 +03:00
Ali BARIN
8a09408cf5 chore: exclude disabled apps from typecheck 2022-10-07 16:06:10 +03:00
Ali BARIN
45c0995d9d refactor: rewrite scheduler as functional 2022-10-07 16:06:10 +03:00
Ali BARIN
0f3d1f0173 fix: add empty type definitions in slack and twitter 2022-10-07 16:06:10 +03:00
Ali BARIN
a83983fe06 fix: add BASE_URL in icons of slack and twitter 2022-10-07 16:06:10 +03:00
Ali BARIN
b3937af636 refactor: rewrite getApp helper with smaller funcs 2022-10-07 16:06:10 +03:00
Ali BARIN
5037e67857 fix: serve assets synchronously 2022-10-07 16:06:10 +03:00
Ali BARIN
ae76f7100c refactor(web): fix types 2022-10-07 16:06:10 +03:00
Faruk AYDIN
0825eb36e4 chore: Use get app helper to get application data 2022-10-07 16:06:10 +03:00
Faruk AYDIN
a58a59788d refactor: Redesign slack actions 2022-10-07 16:06:10 +03:00
Faruk AYDIN
77624acc01 refactor: Redesign twitter trigger and actions 2022-10-07 16:06:10 +03:00
Faruk AYDIN
63ffd1f720 fix: Type problems regarding global variable 2022-10-07 16:06:10 +03:00
Faruk AYDIN
8308265a62 wip: Restructure twitter integration 2022-10-07 16:06:10 +03:00
Faruk AYDIN
dc0e03245f fix: Type errors related with global variable of connection 2022-10-07 16:06:10 +03:00
Faruk AYDIN
67cb57e7ac chore: Add the type for global variable of connection 2022-10-07 16:06:10 +03:00
Faruk AYDIN
4d5be952ce refactor: Use functional design for the authentication of apps 2022-10-07 16:06:10 +03:00
Ömer Faruk Aydın
0abab06124 Merge pull request #558 from automatisch/fix/typo
fix: Typo for the http client creation method
2022-10-06 00:30:01 +03:00
Faruk AYDIN
048689d28f fix: Typo for the http client creation method 2022-10-04 04:07:12 +03:00
Ömer Faruk Aydın
0be4b74d0e Merge pull request #555 from automatisch/refactor-http-client
refactor: rewrite http-client as functional
2022-10-04 01:43:22 +03:00
Ömer Faruk Aydın
26ed5cc794 Merge pull request #556 from automatisch/translations-in-app-event-substep
feat(ChooseAppAndEventSubstep): add translations
2022-10-03 20:49:27 +03:00
Ali BARIN
d236494c0b feat(ChooseAppAndEventSubstep): add translations 2022-10-03 19:45:08 +02:00
Ali BARIN
52e4e09e85 refactor: rewrite http-client as functional 2022-10-03 17:10:16 +02:00
Ömer Faruk Aydın
3b8a12810c Merge pull request #552 from automatisch/fix/expose-errors-as-execute-flow
fix: Expose errors within execute flow mutation
2022-09-28 10:42:41 +03:00
Ali BARIN
e5bd4228b2 feat(TestSubstep): prettify error body 2022-09-28 09:38:18 +02:00
Faruk AYDIN
8e60f3dfd7 fix: Expose errors within execute flow mutation 2022-09-27 23:33:58 +03:00
Ömer Faruk Aydın
18f315d26c Merge pull request #551 from automatisch/issue-536
Show continue upon successful test and toggle next step upon continuing
2022-09-27 23:33:18 +03:00
Ali BARIN
fca140e3c9 feat(Editor): toggle next step upon continuing 2022-09-27 21:52:49 +02:00
Ali BARIN
330902ccb3 feat(TestSubstep): show continue upon successful test 2022-09-27 21:52:23 +02:00
Ömer Faruk Aydın
932994622e Merge pull request #549 from automatisch/issue-547
fix(Processor): correct variable matching regex
2022-09-27 20:07:47 +03:00
Ömer Faruk Aydın
37a60e3105 Merge pull request #550 from automatisch/issue-494
docs: describe user token for Slack connection
2022-09-27 20:02:33 +03:00
Ali BARIN
bce11819ff docs: describe user token for Slack connection 2022-09-27 18:48:28 +02:00
Ali BARIN
d97fab28e3 fix(Processor): correct variable matching regex 2022-09-27 17:22:31 +02:00
Ömer Faruk Aydın
620629ff8b Merge pull request #548 from automatisch/feature/action-errors
feat: Implement exposing errors from actions within processor
2022-09-27 13:24:53 +03:00
Faruk AYDIN
e149d47135 feat: Implement exposing errors from actions within processor 2022-09-27 12:20:16 +03:00
Ömer Faruk Aydın
45cce9956b Merge pull request #546 from automatisch/issue-541
feat(PowerInput): show variable suggestions when focused
2022-09-27 03:03:12 +03:00
Ali BARIN
dd003845af fix: shutdown server on SIGTERM 2022-09-26 22:36:12 +02:00
Ali BARIN
93b31c10ae feat(PowerInput): show variable suggestions when focused 2022-09-26 21:53:48 +02:00
Ömer Faruk Aydın
c9056516a1 Merge pull request #545 from automatisch/errors
Store and expose integration errors
2022-09-26 19:59:30 +03:00
Faruk AYDIN
953a031ba6 chore: Use integrationError instead of automatischError 2022-09-26 19:12:07 +03:00
Faruk AYDIN
e2f04441c5 feat: Remove associated execution steps while deleting the step 2022-09-26 18:02:52 +02:00
Faruk AYDIN
8826125845 feat: Draft implementation to store and expose integration errors 2022-09-26 17:05:56 +03:00
Ali BARIN
d0bcf997fb feat: show error in execution step 2022-09-26 17:01:04 +03:00
Ömer Faruk Aydın
9429031a61 Merge pull request #543 from automatisch/fix/step-with-test-executions
fix: Adjust variables with successful step executions
2022-09-26 11:22:28 +03:00
Faruk AYDIN
556cdc17bd fix: Adjust variables with successful step executions 2022-09-26 03:02:38 +03:00
Ömer Faruk Aydın
db052baea3 Merge pull request #542 from automatisch/docs/contributing
docs: Remove contributing section for the time being
2022-09-26 03:01:06 +03:00
Faruk AYDIN
e858fc2df8 docs: Remove contributing section for the time being 2022-09-26 01:34:00 +03:00
Ömer Faruk Aydın
d5d239937a Merge pull request #540 from automatisch/issue-539
feat(PowerInput/Suggestions): hide steps without execution steps
2022-09-25 22:48:19 +03:00
Ömer Faruk Aydın
68fb34491c Merge pull request #538 from automatisch/issue-537
fix: refetch GetFlow upon altering steps for integrity
2022-09-25 22:47:56 +03:00
Ali BARIN
6ab7265a04 feat(PowerInput/Suggestions): hide steps without execution steps 2022-09-25 20:42:39 +02:00
Ali BARIN
c13248fb44 fix: refetch GetFlow upon altering steps for integrity 2022-09-25 20:35:06 +02:00
Ömer Faruk Aydın
2bc5da885e Merge pull request #535 from automatisch/issue-534
fix(Editor): open new step upon step creation
2022-09-25 20:13:15 +03:00
Ömer Faruk Aydın
1bd7f7a1c5 Merge pull request #523 from automatisch/issue-500
feat(PowerInput): show humanized variable label
2022-09-25 15:32:37 +03:00
Ömer Faruk Aydın
5e8884c3f4 Merge branch 'main' into issue-500 2022-09-25 15:29:43 +03:00
Ali BARIN
ed0d22dcd7 fix(Editor): open new step upon step creation 2022-09-23 18:10:26 +02:00
Faruk AYDIN
7e960024dd fix: Adjust get step with test executions SQL query 2022-09-23 17:55:48 +02:00
Ali BARIN
90f4c873bb feat(PowerInput): show humanized variable label 2022-09-23 17:55:42 +02:00
Ömer Faruk Aydın
1e34ab1ec6 Merge pull request #533 from automatisch/feat/code-of-conduct
chore: Add code of conduct file
2022-09-23 15:40:24 +03:00
Faruk AYDIN
4cf25a8781 chore: Add code of conduct file 2022-09-23 15:30:03 +03:00
Faruk AYDIN
53264e0637 docs: Specify encryption key as environment variable 2022-09-23 00:46:11 +02:00
Ömer Faruk Aydın
7cb3c00709 Merge pull request #531 from automatisch/docs/credentials
docs: Add credentials page
2022-09-22 22:27:46 +03:00
Faruk AYDIN
ef1619fafb docs: Add credentials page 2022-09-22 21:00:54 +03:00
Ömer Faruk Aydın
10b9d69ac8 Merge pull request #530 from automatisch/docs/remove-database
docs: Remove database page
2022-09-22 19:24:31 +03:00
Faruk AYDIN
2223cd7491 docs: Remove database page 2022-09-22 19:05:24 +03:00
Ömer Faruk Aydın
927a0983ee Merge pull request #529 from automatisch/docs/configuration-encryption-key
docs: Add danger tip to encryption key for env variables
2022-09-22 17:52:55 +03:00
Faruk AYDIN
bb752e2bce docs: Add danger tip to encryption key for env variables 2022-09-22 17:50:07 +03:00
Ömer Faruk Aydın
121b99a526 Merge pull request #528 from automatisch/docs/configuration
docs: Add configuration page
2022-09-22 11:43:40 +03:00
Ömer Faruk Aydın
7d325f06f6 Merge pull request #527 from automatisch/docs/connection-page-description
docs: Add description part to the connection pages
2022-09-22 11:43:31 +03:00
Faruk AYDIN
a2d83f5f21 docs: Add configuration page 2022-09-22 02:19:42 +03:00
Faruk AYDIN
85473f6dcf docs: Add description part to the connection pages 2022-09-22 01:48:52 +03:00
Ömer Faruk Aydın
bfb685047e Merge pull request #526 from automatisch/issue-520
feat: show snackbar on flow removal
2022-09-22 01:34:01 +03:00
Ömer Faruk Aydın
416649c8cd Merge pull request #525 from automatisch/feature/add-readme
feat: Add readme file
2022-09-22 01:32:20 +03:00
Ali BARIN
aef77dde59 feat: show snackbar on flow removal 2022-09-22 00:27:43 +02:00
Faruk AYDIN
e1354c15fc docs: Fix wording for advantages of Automatisch 2022-09-22 00:57:50 +03:00
Faruk AYDIN
b41fca7e6c feat: Add readme file 2022-09-22 00:57:50 +03:00
Ömer Faruk Aydın
dc89da2420 Merge pull request #524 from automatisch/fix/license-wording
docs: Fix wording for license page
2022-09-22 00:49:52 +03:00
Faruk AYDIN
1a1e7e57fb docs: Fix wording for license page 2022-09-22 00:44:39 +03:00
Ömer Faruk Aydın
cc0c73f226 Merge pull request #522 from automatisch/chore/docker-service-name
chore: Use main instead of automatisch service in docker compose file
2022-09-21 19:16:48 +03:00
Faruk AYDIN
3c46f747b5 chore: Use main instead of automatisch service in docker compose file 2022-09-21 19:08:08 +03:00
Ömer Faruk Aydın
0f8e4613da Merge pull request #521 from automatisch/chore/replace-initial-letter
chore: Use upper case letter for the initial of Automatisch
2022-09-21 17:56:10 +03:00
Faruk AYDIN
801052adcd chore: Use upper case letter for the initial of Automatisch 2022-09-21 17:50:09 +03:00
Faruk AYDIN
70af9981e7 feat: Implement flow removal with associated records 2022-09-21 09:11:03 +02:00
Faruk AYDIN
715c900d88 docs: Specify user can change also email from settings page 2022-09-21 09:09:05 +02:00
Faruk AYDIN
8162ab4299 docs: Add service type to telemetry page 2022-09-21 09:08:51 +02:00
Faruk AYDIN
a2eae0da6d docs: Remind to set telemetry enabled env var in docker-compose file 2022-09-21 09:08:51 +02:00
Faruk AYDIN
832876d711 feat: Add service type to telemetry data 2022-09-21 09:08:51 +02:00
Faruk AYDIN
4d7ee3dcca feat: Toggle telemetry with telemetry enabled env variable 2022-09-21 09:08:51 +02:00
Faruk AYDIN
6a8c642b72 docs: Add warning for limited available apps 2022-09-21 09:08:17 +02:00
Ömer Faruk Aydın
fe200fb790 Merge pull request #515 from automatisch/issue-510
fix: disable adding new step in published flow
2022-09-21 01:23:27 +03:00
Ali BARIN
601582002a fix: disable adding new step in published flow 2022-09-20 20:52:28 +02:00
Faruk AYDIN
81349bfdb3 docs: Add telemetry page 2022-09-20 15:51:28 +02:00
Ömer Faruk Aydın
ab71dd2974 Merge pull request #512 from automatisch/docs/homepage-image-radius
docs: Add border-radius to screenshot image of homepage
2022-09-20 12:23:34 +03:00
Ömer Faruk Aydın
40dfbae70c Merge pull request #513 from automatisch/docs/reword-automatisch
docs: Use available apps to define what is Automatisch
2022-09-20 12:23:22 +03:00
Faruk AYDIN
31613225af docs: Use available apps to define what is Automatisch 2022-09-20 11:57:51 +03:00
Faruk AYDIN
09b3f96189 docs: Add border-radius to screenshot image of homepage 2022-09-20 11:42:16 +03:00
Ömer Faruk Aydın
cfef769604 Merge pull request #511 from automatisch/docs/flow-ss
docs: Add flows screenshot to homepage of docs
2022-09-20 02:39:51 +03:00
Faruk AYDIN
364e69600c docs: Add flows screenshot to homepage of docs 2022-09-20 02:12:42 +03:00
Ömer Faruk Aydın
b57f0735c9 Merge pull request #509 from automatisch/fix-scheduler
fix: provide step.parameters to Scheduler triggers
2022-09-18 17:06:09 +03:00
Ali BARIN
58565fddae fix: provide step.parameters to Scheduler triggers 2022-09-18 14:58:22 +02:00
Ali BARIN
4183884537 feat: show active flows first 2022-09-18 14:35:42 +02:00
Faruk AYDIN
8c12d2dc85 docs: Remove github and typeform from available apps 2022-09-16 08:19:43 +02:00
Faruk AYDIN
b8970fc3f8 docs: Hide typeform and github connection setups 2022-09-16 08:19:43 +02:00
Ömer Faruk Aydın
dc8a32b0b4 Merge pull request #507 from automatisch/issue-499
feat: show poll interval in trigger step
2022-09-14 01:12:10 +03:00
Ali BARIN
02deed3e3a feat: show poll interval in trigger step 2022-09-13 23:39:10 +02:00
Ömer Faruk Aydın
e50f641533 Merge pull request #504 from automatisch/issue-498
feat: show CTA for connecting to services
2022-09-13 21:32:12 +03:00
Ali BARIN
bd754da1ed feat: show CTA for connecting to services 2022-09-13 20:26:49 +02:00
Ömer Faruk Aydın
23c568c87c Merge pull request #506 from automatisch/docs/installation
docs: Remind that users can change password after installation
2022-09-13 20:20:21 +03:00
Faruk AYDIN
cc9b047300 docs: Remind that users can change password after installation 2022-09-13 19:23:40 +03:00
Ömer Faruk Aydın
eb3ac23c01 Merge pull request #505 from automatisch/docs/other-pages
License and community pages of docs
2022-09-13 18:47:52 +03:00
Faruk AYDIN
5358d6ce5d docs: Add community page 2022-09-13 18:38:22 +03:00
Faruk AYDIN
c0eaab3254 docs: Add license page 2022-09-13 18:37:16 +03:00
Ömer Faruk Aydın
2a39435413 Merge pull request #503 from automatisch/docs/key-concepts
docs: Revise key concepts page
2022-09-13 18:35:06 +03:00
Faruk AYDIN
01ea316c1f docs: Revise key concepts page 2022-09-13 18:30:51 +03:00
Faruk AYDIN
b253040d0a docs: Complete installation doc 2022-09-12 10:12:50 +02:00
Faruk AYDIN
57748a3541 docs: Revise homepage copy of the documentation 2022-09-12 10:12:50 +02:00
Ömer Faruk Aydın
486eb088cf Merge pull request #496 from automatisch/meta-tags
chore: update title, description and manifest
2022-09-12 01:13:08 +03:00
Ömer Faruk Aydın
c07a741970 Merge pull request #497 from automatisch/BASE_URL_in_docs
chore(docs): accept BASE_URL as environment variable
2022-09-12 01:11:44 +03:00
Ali BARIN
d1c7d5ef70 chore(docs): accept BASE_URL as environment variable 2022-09-11 16:20:49 +02:00
Ali BARIN
4b15dad5ea chore(docs): update title and description 2022-09-11 16:03:46 +02:00
Ali BARIN
577e3fc669 chore(web): update title, description and manifest 2022-09-11 16:03:27 +02:00
Ömer Faruk Aydın
b5c7c6d88b Merge pull request #493 from automatisch/issue-492
feat: add find messages action in Slack
2022-09-11 15:25:11 +03:00
Faruk AYDIN
d812bb431f fix: Throw error before getting slack messages for find messages action 2022-09-11 15:21:25 +03:00
Ali BARIN
3593727d29 feat: add find message action in Slack 2022-09-11 12:33:06 +02:00
Ömer Faruk Aydın
75b536959e Merge pull request #491 from automatisch/issue-489
feat(docker-compose): add worker as service
2022-09-08 16:55:09 +03:00
Ali BARIN
29a9338ad7 feat(docker-compose): add worker as service 2022-09-07 23:25:26 +02:00
Ömer Faruk Aydın
3755a3ce4c Merge pull request #490 from automatisch/issue-488
feat(cli): add start-worker command
2022-09-07 20:56:59 +03:00
Ali BARIN
ec0d0203ae feat(cli): add start-worker command 2022-09-07 18:35:20 +02:00
Ömer Faruk Aydın
72e3a69bd9 Merge pull request #487 from automatisch/stop-exposing-psql-redis-in-docker
chore(docker-compose): stop exposing psql & redis
2022-09-07 12:43:55 +03:00
Ali BARIN
2c61fb0c8b chore(docker-compose): stop exposing psql & redis 2022-09-07 10:58:16 +02:00
dependabot[bot]
56352560a5 chore(deps): bump moment-timezone from 0.5.34 to 0.5.37
Bumps [moment-timezone](https://github.com/moment/moment-timezone) from 0.5.34 to 0.5.37.
- [Release notes](https://github.com/moment/moment-timezone/releases)
- [Changelog](https://github.com/moment/moment-timezone/blob/develop/changelog.md)
- [Commits](https://github.com/moment/moment-timezone/compare/0.5.34...0.5.37)

---
updated-dependencies:
- dependency-name: moment-timezone
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-05 12:09:27 +02:00
Ömer Faruk Aydın
2ac7c17514 Merge pull request #485 from automatisch/issue-484
chore: don't seed user if already seeded
2022-09-05 07:55:07 +03:00
Ali BARIN
a5b3a68588 chore: don't seed user if already seeded 2022-09-05 00:00:03 +02:00
Ömer Faruk Aydın
10dcccf99f Merge pull request #479 from automatisch/feature/add-error-details-column
feat: Add error details jsonb column to execution steps
2022-09-04 19:17:45 +03:00
Faruk AYDIN
d0922d85b3 feat: Add error details jsonb column to execution steps 2022-09-04 19:13:25 +03:00
Ömer Faruk Aydın
fc330e25cf Merge pull request #478 from automatisch/issue-468
feat: add no data alert in test substep and execution
2022-09-02 13:54:34 +03:00
Ali BARIN
dc79d623be feat: add no data alert in test substep and execution 2022-09-01 22:25:57 +02:00
Ömer Faruk Aydın
91f5f1d628 Merge pull request #477 from automatisch/feature/slack-send-a-message-to-channel
refactor: Adjust send a message to channel to use slack client
2022-09-01 23:24:15 +03:00
Faruk AYDIN
e0f055a375 refactor: Adjust send a message to channel to use slack client 2022-09-01 23:20:35 +03:00
Ömer Faruk Aydın
7f3098362a Merge pull request #476 from automatisch/refactor/slack-list-channels
refactor: Adjust slack list channels to use slack client
2022-09-01 22:24:21 +03:00
Faruk AYDIN
26eee1bb63 refactor: Adjust slack list channels to use slack client 2022-09-01 22:18:37 +03:00
Ali BARIN
0246d48584 feat: show test runs in executions 2022-09-01 18:54:58 +02:00
Faruk AYDIN
92053ea25a refactor: Slack authentication by passing flow, connection and step 2022-09-01 17:26:28 +02:00
Ömer Faruk Aydın
d12984f324 Merge pull request #473 from automatisch/chore/limit-available-apps
chore: Remove github and typeform from temporary app list
2022-09-01 14:05:48 +03:00
Faruk AYDIN
0eb28ec1a5 chore: Remove github and typeform from temporary app list 2022-09-01 14:03:31 +03:00
Ömer Faruk Aydın
20f8cca07d Merge pull request #472 from automatisch/feature/twitter-user-followers
feat: Implement new follower of me trigger for twitter
2022-09-01 14:01:59 +03:00
Faruk AYDIN
9613e142c9 feat: Implement new follower of me trigger for twitter 2022-09-01 13:54:33 +03:00
Ömer Faruk Aydın
1cc752b5c7 Merge pull request #470 from automatisch/feature/my-tweets
feat: Implement my tweets trigger
2022-09-01 13:48:48 +03:00
Faruk AYDIN
163629b990 feat: Implement my tweets trigger 2022-09-01 13:34:22 +03:00
Faruk AYDIN
770e115be9 docs: Adjust twitter connection setup wording 2022-08-31 22:41:55 +02:00
Ömer Faruk Aydın
0eb5f3d3e6 Merge pull request #467 from automatisch/wait-for-postgres-in-docker-compose
chore: wait for postgres in docker compose
2022-08-31 22:42:58 +03:00
Ali BARIN
6e9a9992c0 chore: wait for postgres in docker compose 2022-08-31 21:29:05 +02:00
Ömer Faruk Aydın
827fef8a05 Merge pull request #465 from automatisch/refactor/user-tweets
refactor: Use plural wording for user tweets trigger
2022-08-31 19:09:59 +03:00
Ömer Faruk Aydın
b53fcdebe3 Merge pull request #466 from automatisch/docker-compose
chore: add docker-compose support
2022-08-31 19:08:37 +03:00
Faruk AYDIN
6537a1c649 refactor: Use plural wording for user tweets trigger 2022-08-31 19:07:42 +03:00
Faruk AYDIN
abaad7cb82 feat: Implement search tweets trigger 2022-08-31 16:36:39 +02:00
Ali BARIN
ba140f05d3 chore: add docker-compose support 2022-08-31 16:34:15 +02:00
Ömer Faruk Aydın
81a444e056 Merge pull request #462 from automatisch/feature/user-tweet-pagination
feat: Implement draft version of pagination with user tweet trigger
2022-08-31 12:57:26 +03:00
Faruk AYDIN
db2c3556de refactor: Get last execution and find last internal ID 2022-08-31 12:53:19 +03:00
Faruk AYDIN
9bd1447bcf feat: Insert execution even though there is no new data fetched 2022-08-30 15:14:19 +03:00
Faruk AYDIN
fda957b1f6 fix: Adjust response types for twitter auth and endpoints 2022-08-30 14:26:56 +03:00
Faruk AYDIN
29abf702bd chore: Use API Key and API Secret placeholders for twitter connection 2022-08-30 14:18:35 +03:00
Faruk AYDIN
5ddb5ab6fa feat: Implement draft version of pagination with user tweet trigger 2022-08-29 23:11:28 +03:00
Ali BARIN
e997aa6c81 chore: introduce docker compose 2022-08-29 11:45:39 +02:00
Ali BARIN
6271cedc25 feat: stop hiding app bar on scroll 2022-08-25 17:26:47 +02:00
Ali BARIN
743b6d6587 docs: update slack connection path 2022-08-25 16:17:38 +02:00
Ömer Faruk Aydın
ee4303a676 Merge pull request #457 from automatisch/issue-454
feat: add status in flow row
2022-08-25 00:34:07 +03:00
Ali BARIN
c361b9af8d feat: add status in flow row 2022-08-24 23:22:56 +02:00
Ömer Faruk Aydın
f66656fd4e Merge pull request #456 from automatisch/issue-455
feat: make flow editor read only when published
2022-08-24 22:24:44 +03:00
Ali BARIN
be141e55a9 feat: make flow editor read only when published 2022-08-24 21:01:51 +02:00
Ömer Faruk Aydın
5ed3b9230e Merge pull request #453 from automatisch/refactor/flow-step-params
refactor: Pass connection, flow and step as params to apps
2022-08-21 22:03:28 +03:00
Faruk AYDIN
44e3de8534 refactor: Pass connection, flow and step as params to apps 2022-08-21 20:25:04 +03:00
Faruk AYDIN
cd6c5216ff refactor: Adjust create tweet action to use new http client 2022-08-20 11:25:20 +02:00
Ali BARIN
17010f9283 feat: add loading button in test substep 2022-08-20 11:20:38 +02:00
Faruk AYDIN
5fb988ae2d feat: Add published_at column to flows and adjust update flow status 2022-08-17 20:12:18 +02:00
Ömer Faruk Aydın
c4a3f19bba Merge pull request #448 from automatisch/inline-test-errors
feat: inline errors in test substep
2022-08-17 11:21:12 +03:00
Ali BARIN
347c9c9455 feat: inline errors in test substep 2022-08-14 18:35:13 +02:00
Ömer Faruk Aydın
731443ab7d Merge pull request #447 from automatisch/feat/use-vitepress-for-docs
feat: Adjust docs to use vitepress
2022-08-13 19:54:18 +03:00
Faruk AYDIN
898ab41167 fix: Remove broken links for docs 2022-08-13 19:54:10 +03:00
Faruk AYDIN
cf4f5bd084 feat: Change edit url of docs 2022-08-13 19:42:13 +03:00
Faruk AYDIN
c7e55fe3e0 feat: Change docs color set to apply Automatisch colors 2022-08-13 19:38:27 +03:00
Faruk AYDIN
b737cf68ba feat: Adjust docs to use vitepress 2022-08-13 16:54:47 +03:00
Ömer Faruk Aydın
c95622affe Merge pull request #446 from automatisch/feature/show-flow-count-with-connections
feat: Expose flow count with connections of getApp query
2022-08-13 11:59:30 +03:00
Ali BARIN
c1b637b284 feat: display flow count on per connection 2022-08-13 01:39:41 +02:00
Faruk AYDIN
bb37299c5b feat: Expose flow count with connections of getApp query 2022-08-13 01:50:03 +03:00
Ömer Faruk Aydın
2e9d5fb2fc Merge pull request #445 from automatisch/issue-442
fix: reset GQL cache on logout
2022-08-13 00:00:20 +03:00
Ali BARIN
2aeabd4be3 fix: reset GQL cache on logout 2022-08-12 20:56:04 +02:00
Ömer Faruk Aydın
a4a9d60d68 Merge pull request #444 from automatisch/issue-440
feat: add no result found UI on executions
2022-08-12 20:31:21 +03:00
Ömer Faruk Aydın
dac9276b9e Merge pull request #443 from automatisch/issue-439
feat: add no result found UI on apps
2022-08-12 20:30:56 +03:00
Ali BARIN
38700256b0 feat: add no result found UI on executions 2022-08-12 19:19:07 +02:00
Ali BARIN
a5f391d2dc feat: add no result found UI on apps 2022-08-12 18:14:30 +02:00
Ali BARIN
77d7260698 feat: add no result found UI on flows 2022-08-12 18:09:18 +02:00
Ömer Faruk Aydın
782b2bafaa Merge pull request #437 from automatisch/issue-436
feat: add "go back to flows" tooltip in editor
2022-08-12 19:03:06 +03:00
Ali BARIN
a35bee0bc9 feat: add "go back to flows" tooltip in editor 2022-08-12 15:31:47 +02:00
Ömer Faruk Aydın
c7d5584cd9 Merge pull request #435 from automatisch/refactor/user-tweet-trigger
refactor: Adjust architecture for twitter client and user tweet trigger
2022-08-12 00:13:48 +03:00
Faruk AYDIN
bb68b2dea1 refactor: Adjust architecture for twitter client and user tweet trigger 2022-08-12 00:11:06 +03:00
Ömer Faruk Aydın
3b587de138 Merge pull request #434 from automatisch/issue-433
feat: show not found UI in app/connection flows
2022-08-11 22:10:07 +03:00
Ali BARIN
c827ce4270 feat: show not found UI in app/connection flows 2022-08-11 20:26:35 +02:00
Ömer Faruk Aydın
c6c3cbb1d3 Merge pull request #432 from automatisch/issue-426
feat: show related flows for connections
2022-08-11 20:20:49 +03:00
Ali BARIN
ad97fae883 feat: show related flows for connections 2022-08-11 18:44:44 +02:00
Ali BARIN
a5b6e66e22 feat: add header with id, name, date in Execution 2022-08-11 11:00:14 +02:00
Ali BARIN
5d7daa8886 feat: add getExecution query 2022-08-11 11:00:14 +02:00
Ömer Faruk Aydın
47ba42f9f8 Merge pull request #430 from automatisch/issue-425
feat: add connectionId filter in getFlows query
2022-08-10 22:43:32 +03:00
Ali BARIN
04f8a71244 feat: add connectionId filter in getFlows query 2022-08-10 21:21:27 +02:00
Ömer Faruk Aydın
3148118784 Merge pull request #428 from automatisch/issue-427
fix: arrange mobile layout in ExecutionRow
2022-08-09 20:59:38 +03:00
Ali BARIN
b7c4a63d2b fix: arrange mobile layout in ExecutionRow 2022-08-09 19:15:19 +02:00
Ömer Faruk Aydın
a4abd7e1cd Merge pull request #421 from automatisch/fix-pagination-get-execution-steps
fix: remove offset pagination in getExecutionSteps
2022-08-09 18:40:02 +03:00
Ali BARIN
0b01a6386d fix: remove offset pagination in getExecutionSteps 2022-08-09 17:01:20 +02:00
Ömer Faruk Aydın
fa75f11eaf Merge pull request #420 from automatisch/issue-417
fix: make flow.name required with 1 minimum length
2022-08-08 21:08:00 +03:00
Ali BARIN
a1b360a172 fix: make flow.name required with 1 minimum length 2022-08-08 19:44:02 +02:00
Ömer Faruk Aydın
e0a76ea918 Merge pull request #395 from automatisch/issue-369
feat: add non-auth apps and flowCount in getConnectedApps
2022-08-08 20:21:51 +03:00
Ömer Faruk Aydın
7990c68d65 Merge pull request #419 from automatisch/issue-418
fix: update account wording with connection
2022-08-08 19:43:04 +03:00
Faruk AYDIN
8c5d95796f refactor: Adjust getConnectedApps graphql query 2022-08-08 19:39:11 +03:00
Ali BARIN
b901a396bf fix: update account wording with connection 2022-08-08 18:24:36 +02:00
Ali BARIN
fa92375e33 fix: polish top whitespace in executions title 2022-08-07 19:20:04 +02:00
Ali BARIN
04b3d93d5d feat: create action by default on flow creation 2022-08-07 19:19:53 +02:00
Ömer Faruk Aydın
1f8bc9cfbd Merge pull request #411 from automatisch/issue-383
feat: perform remote search on flows
2022-08-07 16:42:54 +03:00
Ali BARIN
71939f9163 feat: perform remote search on flows 2022-08-07 15:39:47 +02:00
Ömer Faruk Aydın
0ea8f3a01a Merge pull request #410 from automatisch/issue-384
feat: add search capability by name in getFlows
2022-08-07 16:23:31 +03:00
Ömer Faruk Aydın
ffa49149c9 Merge pull request #409 from automatisch/issue-380
feat: add flow app icons and relative date in execution row
2022-08-07 16:23:00 +03:00
Ömer Faruk Aydın
40a35691fe Merge pull request #408 from automatisch/issue-406
feat: add pagination on app flows
2022-08-07 16:21:32 +03:00
Ömer Faruk Aydın
908a3126f1 Merge pull request #407 from automatisch/issue-375
refactor: unify flow rows
2022-08-07 16:20:53 +03:00
Ali BARIN
726707afe8 feat: add search capability by name in getFlows 2022-08-07 15:10:11 +02:00
Ali BARIN
3c926adeca feat: add relative date in execution row 2022-08-07 14:56:05 +02:00
Ali BARIN
fc681d9ebc feat: add flow app icons in execution row 2022-08-07 14:50:16 +02:00
Ali BARIN
364f53142c feat: add pagination on app flows 2022-08-07 14:34:58 +02:00
Ali BARIN
2764aa2c06 refactor: unify flow rows 2022-08-07 14:13:52 +02:00
Ali BARIN
ca141b1076 feat: downsize flow app icons 2022-08-07 12:17:34 +02:00
Faruk AYDIN
2e980664ac fix: Order associated steps by position for the flow 2022-08-07 11:54:44 +02:00
Ali BARIN
03cfa8b904 fix: remove last non-existing step app icon 2022-08-07 00:26:19 +02:00
Ömer Faruk Aydın
c5bb07a768 Merge pull request #400 from automatisch/fix/flows-group-by
fix: Add group by to differentiate flows
2022-08-07 01:24:12 +03:00
Faruk AYDIN
3a1e8b4bbd fix: Add group by to differentiate flows 2022-08-07 01:21:24 +03:00
Ömer Faruk Aydın
0e749936a6 Merge pull request #373 from automatisch/issue-367
Enhance flow row with used apps, date and context menu
2022-08-07 01:11:22 +03:00
Ali BARIN
744b31aad6 feat: add dynamic flow app icons 2022-08-06 23:41:47 +02:00
Ali BARIN
82bdf9d3b1 feat: add intermediate step count in flow apps 2022-08-06 23:41:47 +02:00
Ali BARIN
8c59bd664e feat: add used app icons in FlowRow 2022-08-06 23:41:47 +02:00
Ali BARIN
15aaada3fe feat: add relative time and context menu in flows 2022-08-06 23:41:47 +02:00
Ömer Faruk Aydın
0fdbe4d39b Merge pull request #399 from automatisch/refactor/flows-query
refactor: Use currentUser with context for flows query
2022-08-06 23:40:50 +03:00
Faruk AYDIN
280c7832ae refactor: Use currentUser with context for flows query 2022-08-06 23:31:09 +03:00
Ömer Faruk Aydın
1f9dd6a3bc Merge pull request #398 from automatisch/feature/expose-icon-url-with-steps
feat: Expose iconUrl together with steps
2022-08-06 23:17:25 +03:00
Faruk AYDIN
e7f12f4a06 feat: Expose iconUrl together with steps 2022-08-06 23:11:42 +03:00
dependabot[bot]
5a24b9ec8a chore(deps): bump parse-url from 6.0.0 to 6.0.5 (#397)
Bumps [parse-url](https://github.com/IonicaBizau/parse-url) from 6.0.0 to 6.0.5.
- [Release notes](https://github.com/IonicaBizau/parse-url/releases)
- [Commits](https://github.com/IonicaBizau/parse-url/compare/6.0.0...6.0.5)

---
updated-dependencies:
- dependency-name: parse-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ömer Faruk Aydın <omerfaruk26@gmail.com>
2022-08-06 19:42:38 +03:00
Ömer Faruk Aydın
1d8a72e03b Merge pull request #394 from automatisch/issue-382
feat: add pagination on /flows
2022-08-06 19:42:23 +03:00
Ömer Faruk Aydın
ef987eae36 Merge pull request #393 from automatisch/issue-389
feat: add paging capability in getFlows query
2022-08-06 19:36:10 +03:00
Faruk AYDIN
dc4899c240 refactor: Adjust flow query to use joinRelated 2022-08-06 15:03:43 +03:00
Ömer Faruk Aydın
88a780f008 Merge pull request #354 from automatisch/dependabot/npm_and_yarn/terser-5.14.2
chore(deps): bump terser from 5.10.0 to 5.14.2
2022-08-06 13:17:13 +03:00
Ömer Faruk Aydın
6fc13f3707 Merge pull request #336 from automatisch/dependabot/npm_and_yarn/parse-url-6.0.2
chore(deps): bump parse-url from 6.0.0 to 6.0.2
2022-08-06 13:16:54 +03:00
Ömer Faruk Aydın
6f1b9b8fe7 Merge pull request #391 from automatisch/issue-374
feat: add loading indicator in AddNewAppConnection
2022-08-06 13:16:23 +03:00
Ali BARIN
533d73d718 feat: add non-auth apps and flowCount in getConnectedApps 2022-08-05 16:13:50 +02:00
Ali BARIN
63241d2438 feat: add pagination on /flows 2022-08-05 15:21:21 +02:00
Ali BARIN
5a177b330a feat: add paging capability in getFlows query 2022-08-05 13:40:03 +02:00
Ali BARIN
ae8f701e5c fix: use cache-and-network fetchPolicy for consistency 2022-08-04 22:04:36 +02:00
Ali BARIN
744c927b69 fix: use non-draft connectionCount in getConnectedApps 2022-08-04 22:04:36 +02:00
Ali BARIN
6ca1b99947 fix: hide form field error if not touched 2022-08-04 21:56:00 +02:00
Ali BARIN
cdb018dcd2 feat: update color scheme in JSONViewer 2022-08-04 21:55:50 +02:00
Ali BARIN
03efe3f0b3 fix: replace current history entry on redirections 2022-08-04 21:36:18 +02:00
Ali BARIN
913a2773c1 feat: make errors inline in add app connection 2022-08-04 21:33:59 +02:00
Ali BARIN
10c64167d7 feat: link typography logo to dashboard 2022-08-04 21:33:49 +02:00
Ali BARIN
a624a8d8b5 refactor: add version in app config 2022-08-04 21:31:09 +02:00
Ali BARIN
ff09a836b4 feat: highlight newer versions in notifications 2022-08-04 21:31:09 +02:00
Ali BARIN
e7c734c55e feat: expose version with healthcheck query 2022-08-04 21:31:09 +02:00
Ali BARIN
5271af8b94 feat: add supportsConnections support in App 2022-08-04 21:23:23 +02:00
Ali BARIN
df55d9fdd9 feat: add loading indicator in AddNewAppConnection 2022-08-03 21:03:22 +02:00
Ali BARIN
c7ff9dc162 chore: upgrade @apollo/client to 3.6.9 2022-08-03 21:02:53 +02:00
Ömer Faruk Aydın
f7ab2b667c Merge pull request #364 from automatisch/docs/twitter-auth
docs: Twitter authentication guide
2022-07-28 00:32:11 +03:00
Ömer Faruk Aydın
b89086d3b8 Merge pull request #366 from automatisch/feature/connection-draft-column
feat: Add draft column to connections
2022-07-27 23:28:44 +03:00
Faruk AYDIN
448a1a49b4 fix: Import App model for create connection query 2022-07-27 23:25:20 +03:00
Ali BARIN
98d7969a1e feat: check app existence in createConnection 2022-07-27 21:11:31 +02:00
Ali BARIN
d513e03138 feat: show connection upon verification 2022-07-27 16:40:21 +02:00
Ali BARIN
a5367c3770 feat: add app field in verifyConnection 2022-07-27 16:01:39 +02:00
Ali BARIN
58d5847eed refactor: remove app field out of createConnection 2022-07-27 16:01:19 +02:00
Faruk AYDIN
02b22740b2 docs: Add twitter authentication guide 2022-07-27 13:55:14 +03:00
Faruk AYDIN
70d59c6c64 feat: Add draft column to connections 2022-07-27 13:54:10 +03:00
Ömer Faruk Aydın
095948e737 Merge pull request #363 from automatisch/fix-diagnostic-track
fix: keep this in diagnosticInfo timeout
2022-07-27 12:03:00 +03:00
Ali BARIN
e6cec355cc fix: keep this in diagnosticInfo timeout 2022-07-27 10:28:17 +02:00
Ömer Faruk Aydın
351f152664 Merge pull request #362 from automatisch/refactor/use-twitter-api-v2-to-verify
refactor: Use twitter api v2 endpoint to verify authentication
2022-07-27 11:20:16 +03:00
Faruk AYDIN
04450b8793 refactor: Use twitter api v2 endpoint to verify authentication 2022-07-27 00:49:19 +03:00
Ömer Faruk Aydın
76612e81b2 Merge pull request #361 from automatisch/refactor/remove-redundant-get-app-connections
refactor: Remove redundant getAppConnections query
2022-07-27 00:21:11 +03:00
Faruk AYDIN
cb26948db6 refactor: Remove redundant getAppConnections query 2022-07-26 23:42:00 +03:00
Ömer Faruk Aydın
18cdd226bb Merge pull request #357 from automatisch/fix/typeform-connection-typo
docs: Fix typeform connection typo
2022-07-23 20:43:42 +03:00
Faruk AYDIN
2199970e50 docs: Fix typeform connection typo 2022-07-23 20:41:00 +03:00
Ömer Faruk Aydın
8389c9fdad Merge pull request #356 from automatisch/docs/typeform-connection-guide
docs: Add guide for typeform connection setup
2022-07-23 20:22:27 +03:00
Faruk AYDIN
0d06ec9a22 docs: Add guide for typeform connection setup 2022-07-23 20:19:13 +03:00
Ömer Faruk Aydın
ce56f166cc Merge pull request #355 from automatisch/docs/request-integration
docs: Add request an integration guide
2022-07-20 16:43:44 +03:00
Faruk AYDIN
a07938d517 docs: Add request an integration guide 2022-07-20 16:38:55 +03:00
dependabot[bot]
7d81a4bdd2 chore(deps): bump terser from 5.10.0 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.10.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 13:11:09 +00:00
Ömer Faruk Aydın
c4bffe7f9d Merge pull request #353 from automatisch/docs/slack-connection-guide
docs: Add setup guide for slack connection
2022-07-20 16:03:43 +03:00
Faruk AYDIN
0c2caccc7c docs: Add setup guide for slack connection 2022-07-20 16:00:10 +03:00
Ömer Faruk Aydın
d3c9f7a491 Merge pull request #352 from automatisch/docs/scheduler-connection
docs: Add an explanation of scheduler connection
2022-07-20 14:17:56 +03:00
Faruk AYDIN
f09fa0fe7c docs: Add an explanation of scheduler connection 2022-07-20 14:15:05 +03:00
Ömer Faruk Aydın
8f1fbf086f Merge pull request #351 from automatisch/chore/change-docs-sidebar-order
chore: Change sidebar order for docs
2022-07-20 14:11:11 +03:00
Faruk AYDIN
272c666d23 chore: Change sidebar order for docs 2022-07-20 13:16:09 +03:00
Ömer Faruk Aydın
a1db89700b Merge pull request #350 from automatisch/docs/github-connection
docs: Add how to create github connection guide
2022-07-20 13:15:27 +03:00
Faruk AYDIN
311f0a747d docs: Add how to create github connection guide 2022-07-20 13:10:12 +03:00
Ömer Faruk Aydın
69298857a9 Merge pull request #349 from automatisch/docs/connections
docs: Add draft version of connections page
2022-07-20 12:29:25 +03:00
Faruk AYDIN
c98680fa59 docs: Add draft version of connections page 2022-07-20 12:23:45 +03:00
Ömer Faruk Aydın
5f357afcd6 Merge pull request #348 from automatisch/refactor/use-http-client-for-twitter-oauth
refactor: Adjust twitter authentication to use http client
2022-07-19 18:33:34 +03:00
Faruk AYDIN
997775e54b refactor: Adjust twitter authentication to use http client 2022-07-19 16:45:47 +03:00
Ömer Faruk Aydın
2cf79e27de Merge pull request #347 from automatisch/refactor/use-http-client-for-github-auth
refactor: Use http client to authenticate github
2022-07-18 01:34:41 +03:00
Faruk AYDIN
95d03e00da refactor: Use http client to authenticate github 2022-07-18 01:31:07 +03:00
Ömer Faruk Aydın
c85aadf006 Merge pull request #345 from automatisch/refactor/use-http-client-for-slack-auth
refactor: Use http client for slack authentication
2022-07-17 00:46:53 +03:00
Faruk AYDIN
e9ffb7ef82 refactor: Use http client for slack authentication 2022-07-17 00:44:47 +03:00
Ömer Faruk Aydın
4237972c86 Merge pull request #344 from automatisch/chore/adjust-connections-order
chore: Adjust connections order by created date descending
2022-07-16 23:40:50 +03:00
Faruk AYDIN
b69009f8b6 chore: Adjust connections order by created date descending 2022-07-16 21:38:35 +03:00
Ömer Faruk Aydın
6fa6ee7a1b Merge pull request #342 from automatisch/refactor/use-http-client-for-typeform-auth
refactor: Use http client to authenticate typeform
2022-07-16 00:57:23 +03:00
Faruk AYDIN
7cf1bfffbc refactor: Use http client to authenticate typeform 2022-07-16 00:54:55 +03:00
Ömer Faruk Aydın
903b0f52b9 Merge pull request #340 from automatisch/feature/basic-http-client
feat: Implement basic http client
2022-07-15 19:59:03 +03:00
Faruk AYDIN
16e299a12d feat: Implement basic http client 2022-07-15 19:56:48 +03:00
Ömer Faruk Aydın
e950d73742 Merge pull request #338 from automatisch/fix/scheduler-app-type
fix: Add scheduler to AvailableAppsEnumType
2022-07-14 23:44:06 +03:00
Faruk AYDIN
b44cccb972 fix: Add scheduler to AvailableAppsEnumType 2022-07-14 16:26:40 +03:00
Ömer Faruk Aydın
5d4ef1bbe8 Merge pull request #337 from automatisch/chore/restrict-apps
chore: Restrict exposed apps until triggers/actions are complete
2022-07-14 15:37:05 +03:00
Faruk AYDIN
12a6912d97 chore: Restrict exposed apps until triggers/actions are complete 2022-07-14 15:33:03 +03:00
dependabot[bot]
2740f8e23d chore(deps): bump parse-url from 6.0.0 to 6.0.2
Bumps [parse-url](https://github.com/IonicaBizau/parse-url) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/IonicaBizau/parse-url/releases)
- [Commits](https://github.com/IonicaBizau/parse-url/commits)

---
updated-dependencies:
- dependency-name: parse-url
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-06 01:36:01 +00:00
Ömer Faruk Aydın
d0814477eb Merge pull request #334 from automatisch/chore/ts-node-dev
chore: Use ts-node-dev instead of nodemon for backend dev script
2022-05-13 13:02:24 +02:00
Faruk AYDIN
cdb256390c chore: Use ts-node-dev instead of nodemon for backend dev script 2022-05-13 12:58:03 +02:00
Ömer Faruk Aydın
9895cc1488 Merge pull request #333 from automatisch/fix/connection-association
fix: Fetch connection while getting trigger for step
2022-05-13 12:53:23 +02:00
Faruk AYDIN
fad2495941 fix: Fetch connection while getting trigger for step 2022-05-13 12:48:21 +02:00
Ömer Faruk Aydın
f9fa7c4094 Merge pull request #332 from automatisch/feature/env-flag-for-bullmq-dashboard
feat: Enable bullmq dashboard with environment variable
2022-05-13 11:44:30 +02:00
Faruk AYDIN
a5538a07f1 feat: Enable bullmq dashboard with environment variable 2022-05-13 11:40:52 +02:00
Ömer Faruk Aydın
d1b46df78a Merge pull request #331 from automatisch/fix/graceful-shutdown-worker
fix: Implement graceful shutdown for worker and queue scheduler
2022-05-13 10:26:55 +02:00
Faruk AYDIN
57186bf85d fix: Implement graceful shutdown for worker and queue scheduler 2022-05-13 10:22:58 +02:00
298 changed files with 18645 additions and 6856 deletions

View File

@@ -3,6 +3,7 @@ dist
build
coverage
packages/docs/*
packages/e2e-tests
.eslintrc.js
husky.config.js

9
.gitignore vendored
View File

@@ -74,6 +74,15 @@ web_modules/
.env.test
.env.production
# cypress environment variables file
cypress.env.json
# cypress screenshots
packages/e2e-tests/cypress/screenshots
# cypress videos
packages/e2e-tests/cypress/videos/
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
Demonstrating empathy and kindness toward other people
Being respectful of differing opinions, viewpoints, and experiences
Giving and gracefully accepting constructive feedback
Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
The use of sexualized language or imagery, and sexual attention or advances of any kind
Trolling, insulting or derogatory comments, and personal or political attacks
Public or private harassment
Publishing others private information, such as a physical or email address, without their explicit permission
Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ali@automatisch.io. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozillas code of conduct enforcement ladder.
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.

52
README.md Normal file
View File

@@ -0,0 +1,52 @@
# Automatisch - Open Source Zapier Alternative
![Automatisch - Screenshot](https://user-images.githubusercontent.com/2501931/191562539-e42f6c34-03c7-4dc4-bcf9-7f9473a9c64f.png)
🧐 Automatisch is a business automation tool that lets you connect different services like Twitter, Slack, and more to automate your business processes.
💸 Automating your workflows doesn't have to be a difficult or expensive process. You also don't need any programming knowledge to use Automatisch.
## Advantages
There are other existing solutions in the market, like Zapier and Integromat, so you might be wondering why you should use Automatisch.
✅ The most significant advantage of having Automatisch is keeping your data on your own servers. Not all companies want to use an automation service in the cloud, and the current open-source or self-hosted solutions mainly focus on developers rather than a user without a technical background.
🤓 Your contributions are vital to the development of Automatisch. As an open-source software, anyone can have an impact on how it is being developed.
💙 No vendor lock-in. If you ever decide that Automatisch is no longer helpful for your business, you can switch to any other provider, which will be easier than switching from the one cloud provider to another since you have all data and flexibility.
## Documentation
The official documentation can be found here: [https://automatisch.io/docs](https://automatisch.io/docs)
## Installation
```bash
# Clone the repository
git clone git@github.com:automatisch/automatisch.git
# Go to the repository folder
cd automatisch/docker/compose
# Start
docker compose up
```
You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. You can also change your email and password later on from the settings page.
## Community Links
- [Github](https://github.com/automatisch/automatisch)
- [Discord](https://discord.gg/dJSah9CVrC)
- [Twitter](https://twitter.com/automatischio)
## 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.
[https://github.com/automatisch/automatisch/discussions](https://github.com/automatisch/automatisch/discussions)
## License
Automatisch is an open-source software with an [AGPL 3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.md).

View File

@@ -0,0 +1,47 @@
version: "3.9"
services:
main:
build:
context: ../images/wait-for-postgres
network: host
ports:
- "3000:3000"
depends_on:
- postgres
- redis
environment:
- HOST=localhost
- PROTOCOL=http
- PORT=3000
- APP_ENV=production
- REDIS_HOST=redis
- POSTGRES_HOST=postgres
- POSTGRES_DATABASE=automatisch
- POSTGRES_USERNAME=automatisch_user
volumes:
- automatisch_storage:/automatisch/storage
worker:
build:
context: ../images/plain
network: host
depends_on:
- main
environment:
- APP_ENV=production
- REDIS_HOST=redis
- POSTGRES_HOST=postgres
- POSTGRES_DATABASE=automatisch
- POSTGRES_USERNAME=automatisch_user
command: automatisch start-worker --env-file /automatisch/storage/.env
volumes:
- automatisch_storage:/automatisch/storage
postgres:
image: "postgres:14.5"
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DB: automatisch
POSTGRES_USER: automatisch_user
redis:
image: "redis:7.0.4"
volumes:
automatisch_storage:

View File

@@ -0,0 +1,5 @@
# syntax=docker/dockerfile:1
FROM node:16
WORKDIR /automatisch
RUN yarn global add @automatisch/cli@0.1.5

View File

@@ -0,0 +1,15 @@
# syntax=docker/dockerfile:1
FROM node:16
WORKDIR /automatisch
RUN apt-get update && apt-get install -y postgresql-client
COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh
RUN mkdir -p /automatisch/storage
RUN touch /automatisch/storage/.env
RUN echo "ENCRYPTION_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
RUN echo "APP_SECRET_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
RUN yarn global add @automatisch/cli@0.1.5
EXPOSE 3000
CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env

View File

@@ -0,0 +1,11 @@
#!/bin/sh
set -e
until psql -h "$POSTGRES_HOST" -U "$POSTGRES_USERNAME" -d "$POSTGRES_HOST" -c '\q'; do
>&2 echo "Postgres is unavailable - sleeping"
sleep 1
done
>&2 echo "Postgres is up - executing command"
exec "$@"

View File

@@ -2,16 +2,12 @@
"packages": [
"packages/*"
],
"private": true,
"version": "independent",
"version": "0.1.5",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"registry": "http://localhost:5000"
},
"add": {
"exact": true
}
}
}
}

View File

@@ -1,5 +1,6 @@
{
"name": "@automatisch/root",
"license": "AGPL-3.0",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
@@ -31,6 +32,6 @@
"prettier": "^2.5.1"
},
"publishConfig": {
"registry": "http://localhost:5000"
"access": "public"
}
}
}

View File

@@ -13,3 +13,4 @@ ENCRYPTION_KEY=sample-encryption-key
APP_SECRET_KEY=sample-app-secret-key
REDIS_PORT=6379
REDIS_HOST=127.0.0.1
ENABLE_BULLMQ_DASHBOARD=false

View File

@@ -1,11 +1,4 @@
# `backend`
> TODO: description
## Usage
```
const backend = require('backend');
// TODO: DEMONSTRATE API
```
The open source Zapier alternative. Build workflow automation without spending
time and money.

View File

@@ -12,8 +12,14 @@ export async function createUser(email = 'user@automatisch.io', password = 'samp
};
try {
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);
const userCount = await User.query().resultSize();
if (userCount === 0) {
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);
} else {
logger.info('No need to seed a user.');
}
} catch (err) {
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
throw err;

1
packages/backend/database-utils.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './dist/bin/database/utils';

View File

@@ -0,0 +1,2 @@
/* eslint-disable */
module.exports = require('./dist/bin/database/utils');

View File

@@ -1,2 +1 @@
export * as utils from './dist/bin/database/utils';
export * as database from './dist/src/config/database';
export * from './dist/src/config/database';

View File

@@ -1,3 +1,2 @@
/* eslint-disable */
module.exports.utils = require('./dist/bin/database/utils');
module.exports.database = require('./dist/src/config/database');
module.exports = require('./dist/src/config/database');

View File

@@ -1,9 +1,10 @@
{
"name": "@automatisch/backend",
"version": "0.1.0",
"description": "> TODO: description",
"version": "0.1.5",
"license": "AGPL-3.0",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": {
"dev": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec 'ts-node' src/server.ts --ext ts,json",
"dev": "ts-node-dev src/server.ts",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
"build": "tsc && yarn copy-statics",
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
@@ -16,15 +17,16 @@
"db:migration:create": "knex migrate:make",
"db:rollback": "knex migrate:rollback",
"db:migrate": "knex migrate:latest",
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist"
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
"prepack": "yarn build",
"prebuild": "rm -rf ./dist"
},
"dependencies": {
"@automatisch/web": "0.1.0",
"@automatisch/web": "^0.1.5",
"@bull-board/express": "^3.10.1",
"@gitbeaker/node": "^35.6.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",
"@graphql-tools/load": "^7.5.2",
"@octokit/oauth-methods": "^1.2.6",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@slack/bolt": "3.10.0",
"@types/luxon": "^2.3.1",
@@ -53,6 +55,7 @@
"luxon": "2.3.1",
"morgan": "^1.10.0",
"nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6",
"objection": "^3.0.0",
"octokit": "^1.7.1",
"pg": "^8.7.1",
@@ -75,14 +78,19 @@
"test": "__tests__"
},
"files": [
"dist",
"bin",
"src",
"server.js",
"server.d.ts",
"worker.js",
"worker.d.ts",
"logger.js",
"logger.d.ts",
"database.js",
"database.d.ts"
"database.d.ts",
"database-utils.js",
"database-utils.d.ts"
],
"repository": {
"type": "git",
@@ -92,7 +100,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "0.1.0",
"@automatisch/types": "^0.1.5",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/cors": "^2.8.12",
@@ -109,7 +117,8 @@
"ava": "^3.15.0",
"nodemon": "^2.0.13",
"sinon": "^11.1.2",
"ts-node": "^10.2.1"
"ts-node": "^10.2.1",
"ts-node-dev": "^1.1.8"
},
"ava": {
"files": [
@@ -121,5 +130,8 @@
"require": [
"ts-node/register"
]
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -15,13 +15,13 @@ import {
} from './helpers/create-bull-board-handler';
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
if (appConfig.appEnv === 'development') {
if (appConfig.enableBullMQDashboard) {
createBullBoardHandler(serverAdapter);
}
const app = express();
if (appConfig.appEnv === 'development') {
if (appConfig.enableBullMQDashboard) {
injectBullBoardHandler(app, serverAdapter);
}

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/discord/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/discord",
"primaryColor": "5865f2",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/firebase/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/firebase",
"primaryColor": "ffca28",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/flickr/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/flickr",
"primaryColor": "000000",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -222,8 +223,8 @@
"description": "Triggers when you favorite a photo.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "testStep",
@@ -237,8 +238,8 @@
"description": "Triggers when you add a new photo in an album.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -275,8 +276,8 @@
"description": "Triggers when you add a new photo.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "testStep",
@@ -290,8 +291,8 @@
"description": "Triggers when you create a new album.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "testStep",

View File

@@ -4,35 +4,19 @@ import type {
IField,
IJSONObject,
} from '@automatisch/types';
import {
getWebFlowAuthorizationUrl,
exchangeWebFlowCode,
checkToken,
} from '@octokit/oauth-methods';
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
import { URLSearchParams } from 'url';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
scopes: string[] = [
'read:org',
'repo',
'user',
];
client: {
getWebFlowAuthorizationUrl: typeof getWebFlowAuthorizationUrl;
exchangeWebFlowCode: typeof exchangeWebFlowCode;
checkToken: typeof checkToken;
};
scopes: string[] = ['read:org', 'repo', 'user'];
client: IHttpClient;
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = {
getWebFlowAuthorizationUrl,
exchangeWebFlowCode,
checkToken,
};
this.client = createHttpClient({ baseURL: 'https://github.com' });
}
get oauthRedirectUrl(): string {
@@ -42,26 +26,28 @@ export default class Authentication implements IAuthentication {
}
async createAuthData(): Promise<{ url: string }> {
const { url } = await this.client.getWebFlowAuthorizationUrl({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
redirectUrl: this.oauthRedirectUrl,
scopes: this.scopes,
const searchParams = new URLSearchParams({
client_id: this.connectionData.consumerKey as string,
redirect_uri: this.oauthRedirectUrl,
scope: this.scopes.join(','),
});
const url = `https://github.com/login/oauth/authorize?${searchParams.toString()}`;
return {
url: url,
url,
};
}
async verifyCredentials() {
const { data } = await this.client.exchangeWebFlowCode({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
clientSecret: this.connectionData.consumerSecret as string,
code: this.connectionData.oauthVerifier as string,
const response = await this.client.post('/login/oauth/access_token', {
client_id: this.connectionData.consumerKey,
client_secret: this.connectionData.consumerSecret,
code: this.connectionData.oauthVerifier,
});
const data = Object.fromEntries(new URLSearchParams(response.data));
this.connectionData.accessToken = data.access_token;
const tokenInfo = await this.getTokenInfo();
@@ -78,12 +64,23 @@ export default class Authentication implements IAuthentication {
}
async getTokenInfo() {
return this.client.checkToken({
clientType: 'oauth-app',
clientId: this.connectionData.consumerKey as string,
clientSecret: this.connectionData.consumerSecret as string,
token: this.connectionData.accessToken as string,
});
const basicAuthToken = Buffer.from(
this.connectionData.consumerKey + ':' + this.connectionData.consumerSecret
).toString('base64');
const headers = {
Authorization: `Basic ${basicAuthToken}`,
};
const body = {
access_token: this.connectionData.accessToken,
};
return await this.client.post(
`https://api.github.com/applications/${this.connectionData.consumerKey}/token`,
body,
{ headers }
);
}
async isStillVerified() {

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/github/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/github",
"primaryColor": "000000",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",
@@ -19,26 +20,26 @@
},
{
"key": "consumerKey",
"label": "Consumer Key",
"label": "Client ID",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/github#consumer-key",
"docUrl": "https://automatisch.io/docs/github#client-id",
"clickToCopy": false
},
{
"key": "consumerSecret",
"label": "Consumer Secret",
"label": "Client Secret",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/github#consumer-secret",
"docUrl": "https://automatisch.io/docs/github#client-secret",
"clickToCopy": false
}
],
@@ -222,8 +223,8 @@
"description": "Triggers when a new repository is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "testStep",
@@ -237,8 +238,8 @@
"description": "Triggers when a new organization is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "testStep",
@@ -252,8 +253,8 @@
"description": "Triggers when a new branch is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -290,8 +291,8 @@
"description": "Triggers when a new notification is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -329,8 +330,8 @@
"description": "Triggers when a new pull request is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -367,8 +368,8 @@
"description": "Triggers when a new watcher is added to a repo",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -405,8 +406,8 @@
"description": "Triggers when a new milestone is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -443,8 +444,8 @@
"description": "Triggers when a new commit comment is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -481,8 +482,8 @@
"description": "Triggers when a new label is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -519,8 +520,8 @@
"description": "Triggers when a new collaborator is added to a repo",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -557,8 +558,8 @@
"description": "Triggers when a new release is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -595,8 +596,8 @@
"description": "Triggers when a new commit is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",
@@ -656,8 +657,8 @@
"description": "Triggers when a new issue is created",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
"key": "chooseConnection",
"name": "Choose connection"
},
{
"key": "chooseTrigger",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/gitlab/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/gitlab",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/postgresql",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "host",

View File

@@ -0,0 +1,10 @@
const cronTimes = {
everyHour: '0 * * * *',
everyHourExcludingWeekends: '0 * * * 1-5',
everyDayAt: (hour: number) => `0 ${hour} * * *`,
everyDayExcludingWeekendsAt: (hour: number) => `0 ${hour} * * 1-5`,
everyWeekOnAndAt: (weekday: number, hour: number) => `0 ${hour} * * ${weekday}`,
everyMonthOnAndAt: (day: number, hour: number) => `0 ${hour} ${day} * *`,
};
export default cronTimes;

View File

@@ -0,0 +1,14 @@
import { DateTime } from 'luxon';
export default function getDateTimeObjectRepresentation(dateTime: DateTime) {
const defaults = dateTime.toObject();
return {
...defaults,
ISO_date_time: dateTime.toISO(),
pretty_date: dateTime.toLocaleString(DateTime.DATE_MED),
pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS),
pretty_day_of_week: dateTime.toFormat('cccc'),
day_of_week: dateTime.weekday,
};
}

View File

@@ -0,0 +1,10 @@
import { DateTime } from 'luxon';
import cronParser from 'cron-parser';
export default function getNextCronDateTime(cronString: string) {
const cronDate = cronParser.parseExpression(cronString);
const matchingNextCronDateTime = cronDate.next();
const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
return matchingNextDateTime;
};

View File

@@ -1,18 +1,10 @@
import Triggers from './triggers';
import {
IService,
IApp,
IJSONObject,
} from '@automatisch/types';
export default class Scheduler implements IService {
triggers: Triggers;
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.triggers = new Triggers(connectionData, parameters);
}
}
export default {
name: "Scheduler",
key: "scheduler",
iconUrl: "{BASE_URL}/apps/scheduler/assets/favicon.svg",
docUrl: "https://automatisch.io/docs/scheduler",
authDocUrl: "https://automatisch.io/docs/connections/scheduler",
primaryColor: "0059F7",
supportsConnections: false,
requiresAuthentication: false,
};

View File

@@ -1,606 +0,0 @@
{
"name": "Scheduler",
"key": "scheduler",
"iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/scheduler",
"primaryColor": "0059F7",
"requiresAuthentication": false,
"triggers": [
{
"name": "Every hour",
"key": "everyHour",
"description": "Triggers every hour.",
"substeps": [
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Trigger on weekends?",
"key": "triggersOnWeekend",
"type": "dropdown",
"description": "Should this flow trigger on Saturday and Sunday?",
"required": true,
"value": true,
"variables": false,
"options": [
{
"label": "Yes",
"value": true
},
{
"label": "No",
"value": false
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
},
{
"name": "Every day",
"key": "everyDay",
"description": "Triggers every day.",
"substeps": [
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Trigger on weekends?",
"key": "triggersOnWeekend",
"type": "dropdown",
"description": "Should this flow trigger on Saturday and Sunday?",
"required": true,
"value": true,
"variables": false,
"options": [
{
"label": "Yes",
"value": true
},
{
"label": "No",
"value": false
}
]
},
{
"label": "Time of day",
"key": "hour",
"type": "dropdown",
"required": true,
"value": null,
"variables": false,
"options": [
{
"label": "00:00",
"value": 0
},
{
"label": "01:00",
"value": 1
},
{
"label": "02:00",
"value": 2
},
{
"label": "03:00",
"value": 3
},
{
"label": "04:00",
"value": 4
},
{
"label": "05:00",
"value": 5
},
{
"label": "06:00",
"value": 6
},
{
"label": "07:00",
"value": 7
},
{
"label": "08:00",
"value": 8
},
{
"label": "09:00",
"value": 9
},
{
"label": "10:00",
"value": 10
},
{
"label": "11:00",
"value": 11
},
{
"label": "12:00",
"value": 12
},
{
"label": "13:00",
"value": 13
},
{
"label": "14:00",
"value": 14
},
{
"label": "15:00",
"value": 15
},
{
"label": "16:00",
"value": 16
},
{
"label": "17:00",
"value": 17
},
{
"label": "18:00",
"value": 18
},
{
"label": "19:00",
"value": 19
},
{
"label": "20:00",
"value": 20
},
{
"label": "21:00",
"value": 21
},
{
"label": "22:00",
"value": 22
},
{
"label": "23:00",
"value": 23
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
},
{
"name": "Every week",
"key": "everyWeek",
"description": "Triggers every week.",
"substeps": [
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Day of the week",
"key": "weekday",
"type": "dropdown",
"required": true,
"value": null,
"variables": false,
"options": [
{
"label": "Monday",
"value": 1
},
{
"label": "Tuesday",
"value": 2
},
{
"label": "Wednesday",
"value": 3
},
{
"label": "Thursday",
"value": 4
},
{
"label": "Friday",
"value": 5
},
{
"label": "Saturday",
"value": 6
},
{
"label": "Sunday",
"value": 0
}
]
},
{
"label": "Time of day",
"key": "hour",
"type": "dropdown",
"required": true,
"value": null,
"variables": false,
"options": [
{
"label": "00:00",
"value": 0
},
{
"label": "01:00",
"value": 1
},
{
"label": "02:00",
"value": 2
},
{
"label": "03:00",
"value": 3
},
{
"label": "04:00",
"value": 4
},
{
"label": "05:00",
"value": 5
},
{
"label": "06:00",
"value": 6
},
{
"label": "07:00",
"value": 7
},
{
"label": "08:00",
"value": 8
},
{
"label": "09:00",
"value": 9
},
{
"label": "10:00",
"value": 10
},
{
"label": "11:00",
"value": 11
},
{
"label": "12:00",
"value": 12
},
{
"label": "13:00",
"value": 13
},
{
"label": "14:00",
"value": 14
},
{
"label": "15:00",
"value": 15
},
{
"label": "16:00",
"value": 16
},
{
"label": "17:00",
"value": 17
},
{
"label": "18:00",
"value": 18
},
{
"label": "19:00",
"value": 19
},
{
"label": "20:00",
"value": 20
},
{
"label": "21:00",
"value": 21
},
{
"label": "22:00",
"value": 22
},
{
"label": "23:00",
"value": 23
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
},
{
"name": "Every month",
"key": "everyMonth",
"description": "Triggers every month.",
"substeps": [
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Day of the month",
"key": "day",
"type": "dropdown",
"required": true,
"value": null,
"variables": false,
"options": [
{
"label": 1,
"value": 1
},
{
"label": 2,
"value": 2
},
{
"label": 3,
"value": 3
},
{
"label": 4,
"value": 4
},
{
"label": 5,
"value": 5
},
{
"label": 6,
"value": 6
},
{
"label": 7,
"value": 7
},
{
"label": 8,
"value": 8
},
{
"label": 9,
"value": 9
},
{
"label": 10,
"value": 10
},
{
"label": 11,
"value": 11
},
{
"label": 12,
"value": 12
},
{
"label": 13,
"value": 13
},
{
"label": 14,
"value": 14
},
{
"label": 15,
"value": 15
},
{
"label": 16,
"value": 16
},
{
"label": 17,
"value": 17
},
{
"label": 18,
"value": 18
},
{
"label": 19,
"value": 19
},
{
"label": 20,
"value": 20
},
{
"label": 21,
"value": 21
},
{
"label": 22,
"value": 22
},
{
"label": 23,
"value": 23
},
{
"label": 24,
"value": 24
},
{
"label": 25,
"value": 25
},
{
"label": 26,
"value": 26
},
{
"label": 27,
"value": 27
},
{
"label": 28,
"value": 28
},
{
"label": 29,
"value": 29
},
{
"label": 30,
"value": 30
},
{
"label": 31,
"value": 31
}
]
},
{
"label": "Time of day",
"key": "hour",
"type": "dropdown",
"required": true,
"value": null,
"variables": false,
"options": [
{
"label": "00:00",
"value": 0
},
{
"label": "01:00",
"value": 1
},
{
"label": "02:00",
"value": 2
},
{
"label": "03:00",
"value": 3
},
{
"label": "04:00",
"value": 4
},
{
"label": "05:00",
"value": 5
},
{
"label": "06:00",
"value": 6
},
{
"label": "07:00",
"value": 7
},
{
"label": "08:00",
"value": 8
},
{
"label": "09:00",
"value": 9
},
{
"label": "10:00",
"value": 10
},
{
"label": "11:00",
"value": 11
},
{
"label": "12:00",
"value": 12
},
{
"label": "13:00",
"value": 13
},
{
"label": "14:00",
"value": 14
},
{
"label": "15:00",
"value": 15
},
{
"label": "16:00",
"value": 16
},
{
"label": "17:00",
"value": 17
},
{
"label": "18:00",
"value": 18
},
{
"label": "19:00",
"value": 19
},
{
"label": "20:00",
"value": 20
},
{
"label": "21:00",
"value": 21
},
{
"label": "22:00",
"value": 22
},
{
"label": "23:00",
"value": 23
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
]
}

View File

@@ -1,19 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import EveryHour from './triggers/every-hour';
import EveryDay from './triggers/every-day';
import EveryWeek from './triggers/every-week';
import EveryMonth from './triggers/every-month';
export default class Triggers {
everyHour: EveryHour;
everyDay: EveryDay;
everyWeek: EveryWeek;
everyMonth: EveryMonth;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.everyHour = new EveryHour(parameters);
this.everyDay = new EveryDay(parameters);
this.everyWeek = new EveryWeek(parameters);
this.everyMonth = new EveryMonth(parameters);
}
}

View File

@@ -1,40 +0,0 @@
import { DateTime } from 'luxon';
import type { IJSONObject, IJSONValue, ITrigger } from '@automatisch/types';
import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
export default class EveryDay implements ITrigger {
triggersOnWeekend?: boolean;
hour?: number;
constructor(parameters: IJSONObject) {
if (parameters.triggersOnWeekend) {
this.triggersOnWeekend = parameters.triggersOnWeekend as boolean;
}
if (parameters.hour) {
this.hour = parameters.hour as number;
}
}
get interval() {
if (this.triggersOnWeekend) {
return cronTimes.everyDayAt(this.hour);
}
return cronTimes.everyDayExcludingWeekendsAt(this.hour);
}
async run(startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
async testRun() {
const nextCronDateTime = getNextCronDateTime(this.interval);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
}

View File

@@ -0,0 +1,170 @@
import { DateTime } from 'luxon';
import { IGlobalVariable, IJSONValue } from '@automatisch/types';
import cronTimes from '../../common/cron-times';
import getNextCronDateTime from '../../common/get-next-cron-date-time';
import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
export default {
name: 'Every day',
key: 'everyDay',
description: 'Triggers every day.',
substeps: [
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Trigger on weekends?',
key: 'triggersOnWeekend',
type: 'dropdown',
description: 'Should this flow trigger on Saturday and Sunday?',
required: true,
value: true,
variables: false,
options: [
{
label: 'Yes',
value: true
},
{
label: 'No',
value: false
}
]
},
{
label: 'Time of day',
key: 'hour',
type: 'dropdown',
required: true,
value: null,
variables: false,
options: [
{
label: '00:00',
value: 0
},
{
label: '01:00',
value: 1
},
{
label: '02:00',
value: 2
},
{
label: '03:00',
value: 3
},
{
label: '04:00',
value: 4
},
{
label: '05:00',
value: 5
},
{
label: '06:00',
value: 6
},
{
label: '07:00',
value: 7
},
{
label: '08:00',
value: 8
},
{
label: '09:00',
value: 9
},
{
label: '10:00',
value: 10
},
{
label: '11:00',
value: 11
},
{
label: '12:00',
value: 12
},
{
label: '13:00',
value: 13
},
{
label: '14:00',
value: 14
},
{
label: '15:00',
value: 15
},
{
label: '16:00',
value: 16
},
{
label: '17:00',
value: 17
},
{
label: '18:00',
value: 18
},
{
label: '19:00',
value: 19
},
{
label: '20:00',
value: 20
},
{
label: '21:00',
value: 21
},
{
label: '22:00',
value: 22
},
{
label: '23:00',
value: 23
}
]
}
]
},
{
key: 'testStep',
name: 'Test trigger'
}
],
getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
if (parameters.triggersOnWeekend as boolean) {
return cronTimes.everyDayAt(parameters.hour as number);
}
return cronTimes.everyDayExcludingWeekendsAt(parameters.hour as number);
},
async run($: IGlobalVariable, startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

@@ -1,35 +0,0 @@
import { DateTime } from 'luxon';
import type { IJSONObject, IJSONValue, ITrigger } from '@automatisch/types';
import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
export default class EveryHour implements ITrigger {
triggersOnWeekend?: boolean | string;
constructor(parameters: IJSONObject) {
if (parameters.triggersOnWeekend) {
this.triggersOnWeekend = parameters.triggersOnWeekend as string;
}
}
get interval() {
if (this.triggersOnWeekend) {
return cronTimes.everyHour;
}
return cronTimes.everyHourExcludingWeekends;
}
async run(startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
async testRun() {
const nextCronDateTime = getNextCronDateTime(this.interval);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
}

View File

@@ -0,0 +1,64 @@
import { DateTime } from 'luxon';
import { IGlobalVariable, IJSONValue } from '@automatisch/types';
import cronTimes from '../../common/cron-times';
import getNextCronDateTime from '../../common/get-next-cron-date-time';
import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
export default {
name: 'Every hour',
key: 'everyHour',
description: 'Triggers every hour.',
substeps: [
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Trigger on weekends?',
key: 'triggersOnWeekend',
type: 'dropdown',
description: 'Should this flow trigger on Saturday and Sunday?',
required: true,
value: true,
variables: false,
options: [
{
label: 'Yes',
value: true
},
{
label: 'No',
value: false
}
]
}
]
},
{
key: 'testStep',
name: 'Test trigger'
}
],
getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
if (parameters.triggersOnWeekend) {
return cronTimes.everyHour
}
return cronTimes.everyHourExcludingWeekends;
},
async run($: IGlobalVariable, startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

@@ -1,36 +0,0 @@
import { DateTime } from 'luxon';
import type { IJSONObject, IJSONValue, ITrigger } from '@automatisch/types';
import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
export default class EveryMonth implements ITrigger {
day?: number;
hour?: number;
constructor(parameters: IJSONObject) {
if (parameters.day) {
this.day = parameters.day as number;
}
if (parameters.hour) {
this.hour = parameters.hour as number;
}
}
get interval() {
return cronTimes.everyMonthOnAndAt(this.day, this.hour);
}
async run(startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
async testRun() {
const nextCronDateTime = getNextCronDateTime(this.interval);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
}

View File

@@ -0,0 +1,283 @@
import { DateTime } from 'luxon';
import { IGlobalVariable, IJSONValue } from '@automatisch/types';
import cronTimes from '../../common/cron-times';
import getNextCronDateTime from '../../common/get-next-cron-date-time';
import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
export default {
name: 'Every month',
key: 'everyMonth',
description: 'Triggers every month.',
substeps: [
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Day of the month',
key: 'day',
type: 'dropdown',
required: true,
value: null,
variables: false,
options: [
{
label: 1,
value: 1
},
{
label: 2,
value: 2
},
{
label: 3,
value: 3
},
{
label: 4,
value: 4
},
{
label: 5,
value: 5
},
{
label: 6,
value: 6
},
{
label: 7,
value: 7
},
{
label: 8,
value: 8
},
{
label: 9,
value: 9
},
{
label: 10,
value: 10
},
{
label: 11,
value: 11
},
{
label: 12,
value: 12
},
{
label: 13,
value: 13
},
{
label: 14,
value: 14
},
{
label: 15,
value: 15
},
{
label: 16,
value: 16
},
{
label: 17,
value: 17
},
{
label: 18,
value: 18
},
{
label: 19,
value: 19
},
{
label: 20,
value: 20
},
{
label: 21,
value: 21
},
{
label: 22,
value: 22
},
{
label: 23,
value: 23
},
{
label: 24,
value: 24
},
{
label: 25,
value: 25
},
{
label: 26,
value: 26
},
{
label: 27,
value: 27
},
{
label: 28,
value: 28
},
{
label: 29,
value: 29
},
{
label: 30,
value: 30
},
{
label: 31,
value: 31
}
]
},
{
label: 'Time of day',
key: 'hour',
type: 'dropdown',
required: true,
value: null,
variables: false,
options: [
{
label: '00:00',
value: 0
},
{
label: '01:00',
value: 1
},
{
label: '02:00',
value: 2
},
{
label: '03:00',
value: 3
},
{
label: '04:00',
value: 4
},
{
label: '05:00',
value: 5
},
{
label: '06:00',
value: 6
},
{
label: '07:00',
value: 7
},
{
label: '08:00',
value: 8
},
{
label: '09:00',
value: 9
},
{
label: '10:00',
value: 10
},
{
label: '11:00',
value: 11
},
{
label: '12:00',
value: 12
},
{
label: '13:00',
value: 13
},
{
label: '14:00',
value: 14
},
{
label: '15:00',
value: 15
},
{
label: '16:00',
value: 16
},
{
label: '17:00',
value: 17
},
{
label: '18:00',
value: 18
},
{
label: '19:00',
value: 19
},
{
label: '20:00',
value: 20
},
{
label: '21:00',
value: 21
},
{
label: '22:00',
value: 22
},
{
label: '23:00',
value: 23
}
]
}
]
},
{
key: 'testStep',
name: 'Test trigger'
}
],
getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
const interval = cronTimes.everyMonthOnAndAt(parameters.day as number, parameters.hour as number);
return interval;
},
async run($: IGlobalVariable, startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

@@ -1,36 +0,0 @@
import { DateTime } from 'luxon';
import type { IJSONObject, IJSONValue, ITrigger } from '@automatisch/types';
import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
export default class EveryWeek implements ITrigger {
weekday?: number;
hour?: number;
constructor(parameters: IJSONObject) {
if (parameters.weekday) {
this.weekday = parameters.weekday as number;
}
if (parameters.hour) {
this.hour = parameters.hour as number;
}
}
get interval() {
return cronTimes.everyWeekOnAndAt(this.weekday, this.hour);
}
async run(startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
async testRun() {
const nextCronDateTime = getNextCronDateTime(this.interval);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return [dateTimeObjectRepresentation] as IJSONValue;
}
}

View File

@@ -0,0 +1,187 @@
import { DateTime } from 'luxon';
import { IGlobalVariable, IJSONValue } from '@automatisch/types';
import cronTimes from '../../common/cron-times';
import getNextCronDateTime from '../../common/get-next-cron-date-time';
import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
export default {
name: 'Every week',
key: 'everyWeek',
description: 'Triggers every week.',
substeps: [
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Day of the week',
key: 'weekday',
type: 'dropdown',
required: true,
value: null,
variables: false,
options: [
{
label: 'Monday',
value: 1
},
{
label: 'Tuesday',
value: 2
},
{
label: 'Wednesday',
value: 3
},
{
label: 'Thursday',
value: 4
},
{
label: 'Friday',
value: 5
},
{
label: 'Saturday',
value: 6
},
{
label: 'Sunday',
value: 0
}
]
},
{
label: 'Time of day',
key: 'hour',
type: 'dropdown',
required: true,
value: null,
variables: false,
options: [
{
label: '00:00',
value: 0
},
{
label: '01:00',
value: 1
},
{
label: '02:00',
value: 2
},
{
label: '03:00',
value: 3
},
{
label: '04:00',
value: 4
},
{
label: '05:00',
value: 5
},
{
label: '06:00',
value: 6
},
{
label: '07:00',
value: 7
},
{
label: '08:00',
value: 8
},
{
label: '09:00',
value: 9
},
{
label: '10:00',
value: 10
},
{
label: '11:00',
value: 11
},
{
label: '12:00',
value: 12
},
{
label: '13:00',
value: 13
},
{
label: '14:00',
value: 14
},
{
label: '15:00',
value: 15
},
{
label: '16:00',
value: 16
},
{
label: '17:00',
value: 17
},
{
label: '18:00',
value: 18
},
{
label: '19:00',
value: 19
},
{
label: '20:00',
value: 20
},
{
label: '21:00',
value: 21
},
{
label: '22:00',
value: 22
},
{
label: '23:00',
value: 23
}
]
}
]
},
{
key: 'testStep',
name: 'Test trigger'
}
],
getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
const interval = cronTimes.everyWeekOnAndAt(parameters.weekday as number, parameters.hour as number);
return interval;
},
async run($: IGlobalVariable, startDateTime: Date) {
const dateTime = DateTime.fromJSDate(startDateTime);
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

@@ -1,32 +0,0 @@
import { DateTime } from 'luxon';
import cronParser from 'cron-parser';
export const cronTimes = {
everyHour: '0 * * * *',
everyHourExcludingWeekends: '0 * * * 1-5',
everyDayAt: (hour: number) => `0 ${hour} * * *`,
everyDayExcludingWeekendsAt: (hour: number) => `0 ${hour} * * 1-5`,
everyWeekOnAndAt: (weekday: number, hour: number) => `0 ${hour} * * ${weekday}`,
everyMonthOnAndAt: (day: number, hour: number) => `0 ${hour} ${day} * *`,
};
export function getNextCronDateTime(cronString: string) {
const cronDate = cronParser.parseExpression(cronString);
const matchingNextCronDateTime = cronDate.next();
const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
return matchingNextDateTime;
};
export function getDateTimeObjectRepresentation(dateTime: DateTime) {
const defaults = dateTime.toObject();
return {
...defaults,
ISO_date_time: dateTime.toISO(),
pretty_date: dateTime.toLocaleString(DateTime.DATE_MED),
pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS),
pretty_day_of_week: dateTime.toFormat('cccc'),
day_of_week: dateTime.weekday,
};
}

View File

@@ -1,13 +0,0 @@
import SendMessageToChannel from './actions/send-message-to-channel';
import { IJSONObject } from '@automatisch/types';
export default class Actions {
sendMessageToChannel: SendMessageToChannel;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.sendMessageToChannel = new SendMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -0,0 +1,50 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type FindMessageOptions = {
query: string;
sortBy: string;
sortDirection: string;
count: number;
};
const findMessage = async ($: IGlobalVariable, options: FindMessageOptions) => {
const message: {
data?: IJSONObject;
error?: IJSONObject;
} = {};
const headers = {
Authorization: `Bearer ${$.auth.data.accessToken}`,
};
const params = {
query: options.query,
sort: options.sortBy,
sort_dir: options.sortDirection,
count: options.count || 1,
};
const response = await $.http.get('/search.messages', {
headers,
params,
});
if (response.integrationError) {
message.error = response.integrationError;
return message;
}
const data = response.data;
if (!data.ok) {
message.error = data;
return message;
}
const messages = data.messages.matches;
message.data = messages?.[0];
return message;
};
export default findMessage;

View File

@@ -0,0 +1,90 @@
import { IGlobalVariable } from '@automatisch/types';
import findMessage from './find-message';
export default {
name: 'Find message',
key: 'findMessage',
description: 'Find a Slack message using the Slack Search feature.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'setupAction',
name: 'Set up action',
arguments: [
{
label: 'Search Query',
key: 'query',
type: 'string',
required: true,
description:
'Search query to use for finding matching messages. See the Slack Search Documentation for more information on constructing a query.',
variables: true,
},
{
label: 'Sort by',
key: 'sortBy',
type: 'dropdown',
description:
'Sort messages by their match strength or by their date. Default is score.',
required: true,
value: 'score',
variables: false,
options: [
{
label: 'Match strength',
value: 'score',
},
{
label: 'Message date time',
value: 'timestamp',
},
],
},
{
label: 'Sort direction',
key: 'sortDirection',
type: 'dropdown',
description:
'Sort matching messages in ascending or descending order. Default is descending.',
required: true,
value: 'desc',
variables: false,
options: [
{
label: 'Descending (newest or best match first)',
value: 'desc',
},
{
label: 'Ascending (oldest or worst match first)',
value: 'asc',
},
],
},
],
},
{
key: 'testStep',
name: 'Test action',
},
],
async run($: IGlobalVariable) {
const parameters = $.db.step.parameters;
const query = parameters.query as string;
const sortBy = parameters.sortBy as string;
const sortDirection = parameters.sortDirection as string;
const count = 1;
const messages = await findMessage($, {
query,
sortBy,
sortDirection,
count,
});
return messages;
},
};

View File

@@ -0,0 +1,59 @@
import { IGlobalVariable } from '@automatisch/types';
import postMessage from './post-message';
export default {
name: 'Send a message to channel',
key: 'sendMessageToChannel',
description: 'Send a message to a specific channel you specify.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'setupAction',
name: 'Set up action',
arguments: [
{
label: 'Channel',
key: 'channel',
type: 'dropdown',
required: true,
description: 'Pick a channel to send the message to.',
variables: false,
source: {
type: 'query',
name: 'getData',
arguments: [
{
name: 'key',
value: 'listChannels',
},
],
},
},
{
label: 'Message text',
key: 'message',
type: 'string',
required: true,
description: 'The content of your new message.',
variables: true,
},
],
},
{
key: 'testStep',
name: 'Test action',
},
],
async run($: IGlobalVariable) {
const channelId = $.db.step.parameters.channel as string;
const text = $.db.step.parameters.message as string;
const message = await postMessage($, channelId, text);
return message;
},
};

View File

@@ -0,0 +1,37 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
const postMessage = async (
$: IGlobalVariable,
channelId: string,
text: string
) => {
const message: {
data: IJSONObject | null | undefined;
error: IJSONObject | null | undefined;
} = {
data: null,
error: null,
};
const headers = {
Authorization: `Bearer ${$.auth.data.accessToken}`,
};
const params = {
channel: channelId,
text,
};
const response = await $.http.post('/chat.postMessage', params, { headers });
message.error = response?.integrationError;
message.data = response?.data?.message;
if (response.data.ok === false) {
message.error = response.data;
}
return message;
};
export default postMessage;

View File

@@ -1,21 +0,0 @@
import { WebClient } from '@slack/web-api';
import { IJSONObject } from '@automatisch/types';
export default class SendMessageToChannel {
client: WebClient;
parameters: IJSONObject;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.client = new WebClient(connectionData.accessToken as string);
this.parameters = parameters;
}
async run() {
const result = await this.client.chat.postMessage({
channel: this.parameters.channel as string,
text: this.parameters.message as string,
});
return result;
}
}

View File

@@ -1,7 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-label="Slack" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -0,0 +1,100 @@
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'accessToken',
label: 'Access Token',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Access token of slack that Automatisch will connect to.',
clickToCopy: false,
},
],
authenticationSteps: [
{
step: 1,
type: 'mutation',
name: 'createConnection',
arguments: [
{
name: 'key',
value: '{key}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
},
],
},
],
},
{
step: 2,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
],
reconnectionSteps: [
{
step: 1,
type: 'mutation',
name: 'resetConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 2,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
},
],
},
],
},
{
step: 3,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
],
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,12 @@
import verifyCredentials from './verify-credentials';
const isStillVerified = async ($: any) => {
try {
await verifyCredentials($);
return true;
} catch (error) {
return false;
}
};
export default isStillVerified;

View File

@@ -0,0 +1,34 @@
import qs from 'qs';
import { IGlobalVariable } from '@automatisch/types';
const verifyCredentials = async ($: IGlobalVariable) => {
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
const stringifiedBody = qs.stringify({
token: $.auth.data.accessToken,
});
const response = await $.http.post('/auth.test', stringifiedBody, {
headers,
});
if (response.data.ok === false) {
throw new Error(
`Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
);
}
const { bot_id: botId, user: screenName } = response.data;
$.auth.set({
botId,
screenName,
token: $.auth.data.accessToken,
});
return response.data;
};
export default verifyCredentials;

View File

@@ -1,39 +0,0 @@
import type { IAuthentication, IApp, IJSONObject } from '@automatisch/types';
import { WebClient } from '@slack/web-api';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: WebClient;
constructor(appData: IApp, connectionData: IJSONObject) {
this.client = new WebClient();
this.connectionData = connectionData;
this.appData = appData;
}
async verifyCredentials() {
const { bot_id: botId, user: screenName } = await this.client.auth.test({
token: this.connectionData.accessToken as string,
});
return {
botId,
screenName,
token: this.connectionData.accessToken,
};
}
async isStillVerified() {
try {
await this.client.auth.test({
token: this.connectionData.accessToken as string,
});
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -1,10 +0,0 @@
import ListChannels from './data/list-channels';
import { IJSONObject } from '@automatisch/types';
export default class Data {
listChannels: ListChannels;
constructor(connectionData: IJSONObject) {
this.listChannels = new ListChannels(connectionData);
}
}

View File

@@ -1,21 +0,0 @@
import type { IJSONObject } from '@automatisch/types';
import { WebClient } from '@slack/web-api';
export default class ListChannels {
client: WebClient;
constructor(connectionData: IJSONObject) {
this.client = new WebClient(connectionData.accessToken as string);
}
async run() {
const { channels } = await this.client.conversations.list();
return channels.map((channel) => {
return {
value: channel.id,
name: channel.name,
};
});
}
}

View File

@@ -0,0 +1,41 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
export default {
name: 'List channels',
key: 'listChannels',
async run($: IGlobalVariable) {
const channels: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const response = await $.http.get('/conversations.list', {
headers: {
Authorization: `Bearer ${$.auth.data.accessToken}`,
},
});
if (response.integrationError) {
channels.error = response.integrationError;
return channels;
}
if (response.data.ok === 'false') {
channels.error = response.data.error;
return channels;
}
channels.data = response.data.channels.map((channel: IJSONObject) => {
return {
value: channel.id,
name: channel.name,
};
});
return channels;
},
};

View File

@@ -1,28 +1,8 @@
import {
IService,
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Triggers from './triggers';
import Actions from './actions';
import Data from './data';
export default class Slack implements IService {
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
data: Data;
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.data = new Data(connectionData);
this.triggers = new Triggers(connectionData, parameters);
this.actions = new Actions(connectionData, parameters);
}
}
export default {
name: 'Slack',
key: 'slack',
iconUrl: '{BASE_URL}/apps/slack/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/connections/slack',
supportsConnections: true,
baseUrl: 'https://slack.com/api',
};

View File

@@ -1,208 +0,0 @@
{
"name": "Slack",
"key": "slack",
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/slack",
"primaryColor": "2DAAE1",
"fields": [
{
"key": "accessToken",
"label": "Access Token",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": "Access token of slack that Automatisch will connect to.",
"docUrl": "https://automatisch.io/docs/slack#access-token",
"clickToCopy": false
}
],
"authenticationSteps": [
{
"step": 1,
"type": "mutation",
"name": "createConnection",
"arguments": [
{
"name": "key",
"value": "{key}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "accessToken",
"value": "{fields.accessToken}"
}
]
}
]
},
{
"step": 2,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{createConnection.id}"
}
]
}
],
"reconnectionSteps": [
{
"step": 1,
"type": "mutation",
"name": "resetConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
},
{
"step": 2,
"type": "mutation",
"name": "updateConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "accessToken",
"value": "{fields.accessToken}"
}
]
}
]
},
{
"step": 3,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
}
],
"triggers": [
{
"name": "New message posted to a channel",
"key": "newMessageToChannel",
"description": "Triggers when a new message is posted to a channel",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Channel",
"key": "channel",
"type": "dropdown",
"required": true,
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listChannels"
}
]
}
},
{
"label": "Trigger for Bot Messages?",
"key": "triggerForBotMessages",
"type": "dropdown",
"description": "Should this flow trigger for bot messages?",
"required": true,
"value": true,
"variables": false,
"options": [
{
"label": "Yes",
"value": true
},
{
"label": "No",
"value": false
}
]
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
],
"actions": [
{
"name": "Send a message to channel",
"key": "sendMessageToChannel",
"description": "Send a message to a specific channel you specify.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "setupAction",
"name": "Set up action",
"arguments": [
{
"label": "Channel",
"key": "channel",
"type": "dropdown",
"required": true,
"description": "Pick a channel to send the message to.",
"variables": false,
"source": {
"type": "query",
"name": "getData",
"arguments": [
{
"name": "key",
"value": "listChannels"
}
]
}
},
{
"label": "Message text",
"key": "message",
"type": "string",
"required": true,
"description": "The content of your new message.",
"variables": true
}
]
},
{
"key": "testStep",
"name": "Test action"
}
]
}
]
}

View File

@@ -1,13 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import NewMessageToChannel from './triggers/new-message-to-channel';
export default class Triggers {
newMessageToChannel: NewMessageToChannel;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newMessageToChannel = new NewMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -1,47 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import axios, { AxiosInstance } from 'axios';
export default class NewMessageToChannel {
httpClient: AxiosInstance;
parameters: IJSONObject;
connectionData: IJSONObject;
BASE_URL = 'https://slack.com/api';
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.httpClient = axios.create({ baseURL: this.BASE_URL });
this.connectionData = connectionData;
this.parameters = parameters;
}
async run() {
// TODO: Fix after webhook implementation.
}
async testRun() {
const headers = {
Authorization: `Bearer ${this.connectionData.accessToken}`,
};
const params = {
channel: this.parameters.channel,
};
const response = await this.httpClient.get('/conversations.history', {
headers,
params,
});
let lastMessage;
if (this.parameters.triggerForBotMessages) {
lastMessage = response.data.messages[0];
} else {
lastMessage = response.data.messages.find(
(message: IJSONObject) =>
!Object.prototype.hasOwnProperty.call(message, 'bot_id')
);
}
return [lastMessage];
}
}

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/smtp/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/smtp",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "host",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/twilio/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twilio",
"primaryColor": "f22f46",
"supportsConnections": true,
"fields": [
{
"key": "accountSid",

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/twitch/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twitch",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -1,10 +0,0 @@
import CreateTweet from './actions/create-tweet';
import { IJSONObject } from '@automatisch/types';
export default class Actions {
createTweet: CreateTweet;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.createTweet = new CreateTweet(connectionData, parameters);
}
}

View File

@@ -1,23 +0,0 @@
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
import { IJSONObject } from '@automatisch/types';
export default class CreateTweet {
client: TwitterApi;
parameters: IJSONObject;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.client = new TwitterApi({
appKey: connectionData.consumerKey,
appSecret: connectionData.consumerSecret,
accessToken: connectionData.accessToken,
accessSecret: connectionData.accessSecret,
} as TwitterApiTokens);
this.parameters = parameters;
}
async run() {
const tweet = await this.client.v1.tweet(this.parameters.tweet as string);
return tweet;
}
}

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Twitter" role="img" viewBox="0 0 512 512">
<rect width="512" height="512" rx="15%" fill="#1da1f2"/>
<path fill="#fff" d="M437 152a72 72 0 01-40 12a72 72 0 0032-40a72 72 0 01-45 17a72 72 0 00-122 65a200 200 0 01-145-74a72 72 0 0022 94a72 72 0 01-32-7a72 72 0 0056 69a72 72 0 01-32 1a72 72 0 0067 50a200 200 0 01-105 29a200 200 0 00309-179a200 200 0 0035-37"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,35 @@
import generateRequest from '../common/generate-request';
import { IJSONObject, IField, IGlobalVariable } from '@automatisch/types';
import { URLSearchParams } from 'url';
export default async function createAuthData($: IGlobalVariable) {
try {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = oauthRedirectUrlField.value;
const response = await generateRequest($, {
requestPath: '/oauth/request_token',
method: 'POST',
data: { oauth_callback: callbackUrl },
});
const responseData = Object.fromEntries(new URLSearchParams(response.data));
await $.auth.set({
url: `${$.app.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
});
} catch (error) {
const errorMessages = error.response.data.errors
.map((error: IJSONObject) => error.message)
.join(' ');
throw new Error(
`Error occured while verifying credentials: ${errorMessages}`
);
}
}

View File

@@ -0,0 +1,219 @@
import createAuthData from './create-auth-data';
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string',
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/twitter/connections/add',
placeholder: null,
description:
'When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.',
clickToCopy: true,
},
{
key: 'consumerKey',
label: 'API Key',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
{
key: 'consumerSecret',
label: 'API Secret',
type: 'string',
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],
authenticationSteps: [
{
step: 1,
type: 'mutation',
name: 'createConnection',
arguments: [
{
name: 'key',
value: '{key}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
],
},
{
step: 2,
type: 'mutation',
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
{
step: 3,
type: 'openWithPopup',
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 4,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.oauth_verifier}',
},
],
},
],
},
{
step: 5,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
],
reconnectionSteps: [
{
step: 1,
type: 'mutation',
name: 'resetConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 2,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
],
},
{
step: 3,
type: 'mutation',
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 4,
type: 'openWithPopup',
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 5,
type: 'mutation',
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'oauthVerifier',
value: '{openAuthPopup.oauth_verifier}',
},
],
},
],
},
{
step: 6,
type: 'mutation',
name: 'verifyConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
],
createAuthData,
verifyCredentials,
isStillVerified,
};

View File

@@ -0,0 +1,13 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
try {
await getCurrentUser($);
return true;
} catch (error) {
return false;
}
};
export default isStillVerified;

View File

@@ -0,0 +1,24 @@
import { IGlobalVariable } from '@automatisch/types';
import { URLSearchParams } from 'url';
const verifyCredentials = async ($: IGlobalVariable) => {
try {
const response = await $.http.post(
`/oauth/access_token?oauth_verifier=${$.auth.data.oauthVerifier}&oauth_token=${$.auth.data.accessToken}`,
null
);
const responseData = Object.fromEntries(new URLSearchParams(response.data));
await $.auth.set({
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
userId: responseData.user_id,
screenName: responseData.screen_name,
});
} catch (error) {
throw new Error(error.response.data);
}
};
export default verifyCredentials;

View File

@@ -1,66 +0,0 @@
import type {
IAuthentication,
IApp,
IField,
IJSONObject,
} from '@automatisch/types';
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: TwitterApi;
constructor(appData: IApp, connectionData: IJSONObject) {
this.appData = appData;
this.connectionData = connectionData;
const clientParams = {
appKey: connectionData.consumerKey,
appSecret: connectionData.consumerSecret,
accessToken: connectionData.accessToken,
accessSecret: connectionData.accessSecret,
} as TwitterApiTokens;
this.client = new TwitterApi(clientParams);
}
async createAuthData() {
const appFields = this.appData.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;
const authLink = await this.client.generateAuthLink(callbackUrl);
return {
url: authLink.url,
accessToken: authLink.oauth_token,
accessSecret: authLink.oauth_token_secret,
};
}
async verifyCredentials() {
const verifiedCredentials = await this.client.login(
this.connectionData.oauthVerifier as string
);
return {
consumerKey: this.connectionData.consumerKey,
consumerSecret: this.connectionData.consumerSecret,
accessToken: verifiedCredentials.accessToken,
accessSecret: verifiedCredentials.accessSecret,
userId: verifiedCredentials.userId,
screenName: verifiedCredentials.screenName,
};
}
async isStillVerified() {
try {
await this.client.currentUser();
return true;
} catch {
return false;
}
}
}

View File

@@ -0,0 +1,44 @@
import { Token } from 'oauth-1.0a';
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import oauthClient from './oauth-client';
import { Method } from 'axios';
type IGenereateRequestOptons = {
requestPath: string;
method: string;
data?: IJSONObject;
};
const generateRequest = async (
$: IGlobalVariable,
options: IGenereateRequestOptons
) => {
const { requestPath, method, data } = options;
const token: Token = {
key: $.auth.data.accessToken as string,
secret: $.auth.data.accessSecret as string,
};
const requestData = {
url: `${$.app.baseUrl}${requestPath}`,
method,
data,
};
const authHeader = oauthClient($).toHeader(
oauthClient($).authorize(requestData, token)
);
const response = await $.http.request({
url: requestData.url,
method: requestData.method as Method,
headers: {
...authHeader,
},
});
return response;
};
export default generateRequest;

View File

@@ -0,0 +1,14 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import generateRequest from './generate-request';
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const response = await generateRequest($, {
requestPath: '/2/users/me',
method: 'GET',
});
const currentUser = response.data.data;
return currentUser;
};
export default getCurrentUser;

View File

@@ -0,0 +1,22 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import generateRequest from './generate-request';
const getUserByUsername = async ($: IGlobalVariable, username: string) => {
const response = await generateRequest($, {
requestPath: `/2/users/by/username/${username}`,
method: 'GET',
});
if (response.data.errors) {
const errorMessages = response.data.errors
.map((error: IJSONObject) => error.detail)
.join(' ');
throw new Error(`Error occured while fetching user data: ${errorMessages}`);
}
const user = response.data.data;
return user;
};
export default getUserByUsername;

View File

@@ -0,0 +1,68 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import { omitBy, isEmpty } from 'lodash';
import generateRequest from './generate-request';
type GetUserFollowersOptions = {
userId: string;
lastInternalId?: string;
};
const getUserFollowers = async (
$: IGlobalVariable,
options: GetUserFollowersOptions
) => {
let response;
const followers: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
do {
const params: IJSONObject = {
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${options.userId}/followers${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
response = await generateRequest($, {
requestPath,
method: 'GET',
});
if (response.integrationError) {
followers.error = response.integrationError;
return followers;
}
if (response.data?.errors) {
followers.error = response.data.errors;
return followers;
}
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (
!options.lastInternalId ||
Number(tweet.id) > Number(options.lastInternalId)
) {
followers.data.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && options.lastInternalId);
return followers;
};
export default getUserFollowers;

View File

@@ -0,0 +1,79 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
import generateRequest from './generate-request';
import getCurrentUser from './get-current-user';
import getUserByUsername from './get-user-by-username';
type IGetUserTweetsOptions = {
currentUser: boolean;
userId?: string;
lastInternalId?: string;
};
const getUserTweets = async (
$: IGlobalVariable,
options: IGetUserTweetsOptions
) => {
let username: string;
if (options.currentUser) {
const currentUser = await getCurrentUser($);
username = currentUser.username as string;
} else {
username = $.db.step.parameters.username as string;
}
const user = await getUserByUsername($, username);
let response;
const tweets: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
do {
const params: IJSONObject = {
since_id: options.lastInternalId,
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${user.id}/tweets${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
response = await generateRequest($, {
requestPath,
method: 'GET',
});
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (
!options.lastInternalId ||
Number(tweet.id) > Number(options.lastInternalId)
) {
tweets.data.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && options.lastInternalId);
return tweets;
};
export default getUserTweets;

View File

@@ -0,0 +1,23 @@
import { IGlobalVariable } from '@automatisch/types';
import crypto from 'crypto';
import OAuth from 'oauth-1.0a';
const oauthClient = ($: IGlobalVariable) => {
const consumerData = {
key: $.auth.data.consumerKey as string,
secret: $.auth.data.consumerSecret as string,
};
return new OAuth({
consumer: consumerData,
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
},
});
};
export default oauthClient;

View File

@@ -1,25 +1,8 @@
import {
IService,
IAuthentication,
IApp,
IJSONObject,
} from '@automatisch/types';
import Authentication from './authentication';
import Triggers from './triggers';
import Actions from './actions';
export default class Twitter implements IService {
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
constructor(
appData: IApp,
connectionData: IJSONObject,
parameters: IJSONObject
) {
this.authenticationClient = new Authentication(appData, connectionData);
this.triggers = new Triggers(connectionData, parameters);
this.actions = new Actions(connectionData, parameters);
}
}
export default {
name: 'Twitter',
key: 'twitter',
iconUrl: '{BASE_URL}/apps/twitter/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/connections/twitter',
supportsConnections: true,
baseUrl: 'https://api.twitter.com',
};

View File

@@ -1,320 +0,0 @@
{
"name": "Twitter",
"key": "twitter",
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twitter",
"primaryColor": "2DAAE1",
"fields": [
{
"key": "oAuthRedirectUrl",
"label": "OAuth Redirect URL",
"type": "string",
"required": true,
"readOnly": true,
"value": "{WEB_APP_URL}/app/twitter/connections/add",
"placeholder": null,
"description": "When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.",
"docUrl": "https://automatisch.io/docs/twitter#oauth-redirect-url",
"clickToCopy": true
},
{
"key": "consumerKey",
"label": "Consumer Key",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/twitter#consumer-key",
"clickToCopy": false
},
{
"key": "consumerSecret",
"label": "Consumer Secret",
"type": "string",
"required": true,
"readOnly": false,
"value": null,
"placeholder": null,
"description": null,
"docUrl": "https://automatisch.io/docs/twitter#consumer-secret",
"clickToCopy": false
}
],
"authenticationSteps": [
{
"step": 1,
"type": "mutation",
"name": "createConnection",
"arguments": [
{
"name": "key",
"value": "{key}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "consumerKey",
"value": "{fields.consumerKey}"
},
{
"name": "consumerSecret",
"value": "{fields.consumerSecret}"
}
]
}
]
},
{
"step": 2,
"type": "mutation",
"name": "createAuthData",
"arguments": [
{
"name": "id",
"value": "{createConnection.id}"
}
]
},
{
"step": 3,
"type": "openWithPopup",
"name": "openAuthPopup",
"arguments": [
{
"name": "url",
"value": "{createAuthData.url}"
}
]
},
{
"step": 4,
"type": "mutation",
"name": "updateConnection",
"arguments": [
{
"name": "id",
"value": "{createConnection.id}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "oauthVerifier",
"value": "{openAuthPopup.oauth_verifier}"
}
]
}
]
},
{
"step": 5,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{createConnection.id}"
}
]
}
],
"reconnectionSteps": [
{
"step": 1,
"type": "mutation",
"name": "resetConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
},
{
"step": 2,
"type": "mutation",
"name": "updateConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "consumerKey",
"value": "{fields.consumerKey}"
},
{
"name": "consumerSecret",
"value": "{fields.consumerSecret}"
}
]
}
]
},
{
"step": 3,
"type": "mutation",
"name": "createAuthData",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
},
{
"step": 4,
"type": "openWithPopup",
"name": "openAuthPopup",
"arguments": [
{
"name": "url",
"value": "{createAuthData.url}"
}
]
},
{
"step": 5,
"type": "mutation",
"name": "updateConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
},
{
"name": "formattedData",
"value": null,
"properties": [
{
"name": "oauthVerifier",
"value": "{openAuthPopup.oauth_verifier}"
}
]
}
]
},
{
"step": 6,
"type": "mutation",
"name": "verifyConnection",
"arguments": [
{
"name": "id",
"value": "{connection.id}"
}
]
}
],
"triggers": [
{
"name": "My Tweet",
"key": "myTweet",
"description": "Will be triggered when you tweet something new.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "testStep",
"name": "Test trigger"
}
]
},
{
"name": "User Tweet",
"key": "userTweet",
"description": "Will be triggered when a specific user tweet something new.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Username",
"key": "username",
"type": "string",
"required": true
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
},
{
"name": "Search Tweet",
"key": "searchTweet",
"description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseTrigger",
"name": "Set up a trigger",
"arguments": [
{
"label": "Search Term",
"key": "searchTerm",
"type": "string",
"required": true
}
]
},
{
"key": "testStep",
"name": "Test trigger"
}
]
}
],
"actions": [
{
"name": "Create Tweet",
"key": "createTweet",
"description": "Will create a tweet.",
"substeps": [
{
"key": "chooseAccount",
"name": "Choose account"
},
{
"key": "chooseAction",
"name": "Set up action",
"arguments": [
{
"label": "Tweet body",
"key": "tweet",
"type": "string",
"required": true,
"description": "The content of your new tweet.",
"variables": true
}
]
},
{
"key": "testStep",
"name": "Test action"
}
]
}
]
}

View File

@@ -1,13 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import MyTweet from './triggers/my-tweet';
import SearchTweet from './triggers/search-tweet';
export default class Triggers {
myTweet: MyTweet;
searchTweet: SearchTweet;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.myTweet = new MyTweet(connectionData);
this.searchTweet = new SearchTweet(connectionData, parameters);
}
}

View File

@@ -1,25 +0,0 @@
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
import { IJSONObject } from '@automatisch/types';
export default class MyTweet {
client: TwitterApi;
constructor(connectionData: IJSONObject) {
this.client = new TwitterApi({
appKey: connectionData.consumerKey,
appSecret: connectionData.consumerSecret,
accessToken: connectionData.accessToken,
accessSecret: connectionData.accessSecret,
} as TwitterApiTokens);
}
async run() {
const response = await this.client.currentUser();
const username = response.screen_name;
const userTimeline = await this.client.v1.userTimelineByUsername(username);
const fetchedTweets = userTimeline.tweets;
return fetchedTweets;
}
}

View File

@@ -0,0 +1,30 @@
import { IGlobalVariable } from '@automatisch/types';
import getUserTweets from '../../common/get-user-tweets';
export default {
name: 'My Tweets',
key: 'myTweets',
pollInterval: 15,
description: 'Will be triggered when you tweet something new.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'testStep',
name: 'Test trigger',
},
],
async run($: IGlobalVariable) {
return await getUserTweets($, {
currentUser: true,
lastInternalId: $.db.flow.lastInternalId,
});
},
async testRun($: IGlobalVariable) {
return await getUserTweets($, { currentUser: true });
},
};

View File

@@ -0,0 +1,27 @@
import { IGlobalVariable } from '@automatisch/types';
import myFollowers from './my-followers';
export default {
name: 'New follower of me',
key: 'myFollowers',
pollInterval: 15,
description: 'Will be triggered when you have a new follower.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'testStep',
name: 'Test trigger',
},
],
async run($: IGlobalVariable) {
return await myFollowers($, $.db.flow.lastInternalId);
},
async testRun($: IGlobalVariable) {
return await myFollowers($);
},
};

View File

@@ -0,0 +1,17 @@
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../../common/get-current-user';
import getUserByUsername from '../../common/get-user-by-username';
import getUserFollowers from '../../common/get-user-followers';
const myFollowers = async ($: IGlobalVariable, lastInternalId?: string) => {
const { username } = await getCurrentUser($);
const user = await getUserByUsername($, username as string);
const tweets = await getUserFollowers($, {
userId: user.id,
lastInternalId,
});
return tweets;
};
export default myFollowers;

View File

@@ -1,58 +0,0 @@
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
import { IJSONObject } from '@automatisch/types';
export default class SearchTweet {
client: TwitterApi;
parameters: IJSONObject;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.client = new TwitterApi({
appKey: connectionData.consumerKey,
appSecret: connectionData.consumerSecret,
accessToken: connectionData.accessToken,
accessSecret: connectionData.accessSecret,
} as TwitterApiTokens);
this.parameters = parameters;
}
async run(startTime: Date) {
const tweets = [];
const response = await this.client.v2.search(
this.parameters.searchTerm as string,
{
max_results: 50,
'tweet.fields': 'created_at',
}
);
for await (const tweet of response.data.data) {
if (new Date(tweet.created_at).getTime() <= startTime.getTime()) {
break;
}
tweets.push(tweet);
if (response.data.meta.next_token) {
await response.fetchNext();
}
}
return tweets;
}
async testRun() {
const response = await this.client.v2.search(
this.parameters.searchTerm as string,
{
max_results: 10,
'tweet.fields': 'created_at',
}
);
const mostRecentTweet = response.data.data[0];
return [mostRecentTweet];
}
}

View File

@@ -0,0 +1,45 @@
import { IGlobalVariable } from '@automatisch/types';
import searchTweets from './search-tweets';
export default {
name: 'Search Tweets',
key: 'searchTweets',
pollInterval: 15,
description:
'Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Search Term',
key: 'searchTerm',
type: 'string',
required: true,
},
],
},
{
key: 'testStep',
name: 'Test trigger',
},
],
async run($: IGlobalVariable) {
return await searchTweets($, {
searchTerm: $.db.step.parameters.searchTerm as string,
lastInternalId: $.db.flow.lastInternalId,
});
},
async testRun($: IGlobalVariable) {
return await searchTweets($, {
searchTerm: $.db.step.parameters.searchTerm as string,
});
},
};

View File

@@ -0,0 +1,70 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import qs from 'qs';
import generateRequest from '../../common/generate-request';
import { omitBy, isEmpty } from 'lodash';
type ISearchTweetsOptions = {
searchTerm: string;
lastInternalId?: string;
};
const searchTweets = async (
$: IGlobalVariable,
options: ISearchTweetsOptions
) => {
let response;
const tweets: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
do {
const params: IJSONObject = {
query: options.searchTerm,
since_id: options.lastInternalId,
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = qs.stringify(omitBy(params, isEmpty));
const requestPath = `/2/tweets/search/recent${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
response = await generateRequest($, {
requestPath,
method: 'GET',
});
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.errors) {
tweets.error = response.data.errors;
return tweets;
}
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (
!options.lastInternalId ||
Number(tweet.id) > Number(options.lastInternalId)
) {
tweets.data.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && options.lastInternalId);
return tweets;
};
export default searchTweets;

View File

@@ -0,0 +1,46 @@
import { IGlobalVariable } from '@automatisch/types';
import getUserTweets from '../../common/get-user-tweets';
export default {
name: 'User Tweets',
key: 'userTweets',
pollInterval: 15,
description: 'Will be triggered when a specific user tweet something new.',
substeps: [
{
key: 'chooseConnection',
name: 'Choose connection',
},
{
key: 'chooseTrigger',
name: 'Set up a trigger',
arguments: [
{
label: 'Username',
key: 'username',
type: 'string',
required: true,
},
],
},
{
key: 'testStep',
name: 'Test trigger',
},
],
async run($: IGlobalVariable) {
return await getUserTweets($, {
currentUser: false,
userId: $.db.step.parameters.username as string,
lastInternalId: $.db.flow.lastInternalId,
});
},
async testRun($: IGlobalVariable) {
return await getUserTweets($, {
currentUser: false,
userId: $.db.step.parameters.username as string,
});
},
};

View File

@@ -5,14 +5,12 @@ import type {
IJSONObject,
} from '@automatisch/types';
import { URLSearchParams } from 'url';
import axios, { AxiosInstance } from 'axios';
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: AxiosInstance = axios.create({
baseURL: 'https://api.typeform.com',
});
client: IHttpClient;
scope: string[] = [
'forms:read',
@@ -27,6 +25,7 @@ export default class Authentication implements IAuthentication {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = createHttpClient({ baseURL: 'https://api.typeform.com' });
}
get oauthRedirectUrl() {

View File

@@ -4,6 +4,7 @@
"iconUrl": "{BASE_URL}/apps/typeform/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/typeform",
"primaryColor": "5865f2",
"supportsConnections": true,
"fields": [
{
"key": "oAuthRedirectUrl",

View File

@@ -13,6 +13,7 @@ type AppConfig = {
postgresHost: string;
postgresUsername: string;
postgresPassword?: string;
version: string;
postgresEnableSsl: boolean;
baseUrl: string;
encryptionKey: string;
@@ -20,12 +21,15 @@ type AppConfig = {
serveWebAppSeparately: boolean;
redisHost: string;
redisPort: number;
enableBullMQDashboard: boolean;
telemetryEnabled: boolean;
};
const host = process.env.HOST || 'localhost';
const protocol = process.env.PROTOCOL || 'http';
const port = process.env.PORT || '3000';
const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
const serveWebAppSeparately =
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
let webAppUrl = `${protocol}://${host}:${port}`;
if (serveWebAppSeparately) {
@@ -42,8 +46,9 @@ const appConfig: AppConfig = {
port,
appEnv: appEnv,
isDev: appEnv === 'development',
version: process.env.npm_package_version,
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresPort: parseInt(process.env.POSTGRES_PORT|| '5432'),
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
postgresHost: process.env.POSTGRES_HOST || 'localhost',
postgresUsername:
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
@@ -54,8 +59,11 @@ const appConfig: AppConfig = {
serveWebAppSeparately,
redisHost: process.env.REDIS_HOST || '127.0.0.1',
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
enableBullMQDashboard:
process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false,
baseUrl,
webAppUrl,
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
};
if (!appConfig.encryptionKey) {

View File

@@ -1,4 +1,8 @@
import process from 'process';
// The following two lines are required to get count values as number.
// More info: https://github.com/knex/knex/issues/387#issuecomment-51554522
import pg from 'pg';
pg.types.setTypeParser(20, 'text', parseInt);
import knex from 'knex';
import type { Knex } from 'knex';
import knexConfig from '../../knexfile';
@@ -8,10 +12,12 @@ export const client: Knex = knex(knexConfig);
const CONNECTION_REFUSED = 'ECONNREFUSED';
client.raw('SELECT 1')
.catch((err) => {
if (err.code === CONNECTION_REFUSED) {
logger.error('Make sure you have installed PostgreSQL and it is running.', err);
process.exit();
}
});
client.raw('SELECT 1').catch((err) => {
if (err.code === CONNECTION_REFUSED) {
logger.error(
'Make sure you have installed PostgreSQL and it is running.',
err
);
process.exit();
}
});

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('connections', (table) => {
table.boolean('draft').defaultTo(true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('connections', (table) => {
table.dropColumn('draft');
});
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('flows', (table) => {
table.timestamp('published_at').nullable();
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('flows', (table) => {
table.dropColumn('published_at');
});
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('executions', (table) => {
table.string('internal_id');
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('executions', (table) => {
table.dropColumn('internal_id');
});
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('execution_steps', (table) => {
table.jsonb('error_details');
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('execution_steps', (table) => {
table.dropColumn('error_details');
});
}

View File

@@ -1,4 +1,6 @@
import Context from '../../types/express/context';
import axios from 'axios';
import globalVariable from '../../helpers/global-variable';
import App from '../../models/app';
type Params = {
@@ -19,22 +21,24 @@ const createAuthData = async (
})
.throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default;
const appData = App.findOneByKey(connection.key);
if (!connection.formattedData) {
return null;
}
if (!connection.formattedData) { return null; }
const authInstance = (await import(`../../apps/${connection.key}/auth`))
.default;
const app = await App.findOneByKey(connection.key);
const appInstance = new appClass(appData, connection.formattedData);
const authLink = await appInstance.authenticationClient.createAuthData();
const $ = await globalVariable(connection, app);
await authInstance.createAuthData($);
await connection.$query().patch({
formattedData: {
...connection.formattedData,
...authLink,
},
});
try {
await axios.get(connection.formattedData.url as string);
} catch (error) {
throw new Error('Error occured while creating authorization URL!');
}
return authLink;
return connection.formattedData;
};
export default createAuthData;

View File

@@ -13,19 +13,12 @@ const createConnection = async (
params: Params,
context: Context
) => {
const app = App.findOneByKey(params.input.key);
await App.findOneByKey(params.input.key);
const connection = await context.currentUser
.$relatedQuery('connections')
.insert({
key: params.input.key,
formattedData: params.input.formattedData,
});
return {
...connection,
app,
};
return await context.currentUser.$relatedQuery('connections').insert({
key: params.input.key,
formattedData: params.input.formattedData,
});
};
export default createConnection;

View File

@@ -4,6 +4,7 @@ import Context from '../../types/express/context';
type Params = {
input: {
triggerAppKey: string;
connectionId: string;
};
};
@@ -12,17 +13,32 @@ const createFlow = async (
params: Params,
context: Context
) => {
const connectionId = params?.input?.connectionId;
const appKey = params?.input?.triggerAppKey;
const flow = await context.currentUser.$relatedQuery('flows').insert({
name: 'Name your flow',
});
if (connectionId) {
await context.currentUser
.$relatedQuery('connections')
.findById(connectionId)
.throwIfNotFound();
}
await Step.query().insert({
flowId: flow.id,
type: 'trigger',
position: 1,
appKey,
connectionId
});
await Step.query().insert({
flowId: flow.id,
type: 'action',
position: 2
});
return flow;

View File

@@ -1,4 +1,6 @@
import Context from '../../types/express/context';
import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step';
type Params = {
input: {
@@ -11,14 +13,23 @@ const deleteFlow = async (
params: Params,
context: Context
) => {
await context.currentUser
const flow = await context.currentUser
.$relatedQuery('flows')
.delete()
.findOne({
id: params.input.id,
})
.throwIfNotFound();
const executionIds = (
await flow.$relatedQuery('executions').select('executions.id')
).map((execution: Execution) => execution.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
await flow.$relatedQuery('executions').delete();
await flow.$relatedQuery('steps').delete();
await flow.$query().delete();
return;
};

View File

@@ -19,8 +19,7 @@ const deleteStep = async (
})
.throwIfNotFound();
if (!step) return;
await step.$relatedQuery('executionSteps').delete();
await step.$query().delete();
const nextSteps = await step.flow

Some files were not shown because too many files have changed in this diff Show More