Compare commits

...

128 Commits

Author SHA1 Message Date
Ali BARIN
b7da85feaa Release v0.1.3 2022-10-08 13:00:08 +02:00
Ali BARIN
a652db7afe chore: upgrade vers in Dockerfiles 2022-10-08 12:58:05 +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
155 changed files with 3115 additions and 3082 deletions

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.

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
# 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)
## Contribution Guide
You can access to the [contribution guide](https://docs.automatisch.io) page from the documentation website.
## License
Automatisch is an open-source software with an [AGPL 3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.md).

View File

@@ -1,6 +1,6 @@
version: "3.9" version: "3.9"
services: services:
automatisch: main:
build: build:
context: ../images/wait-for-postgres context: ../images/wait-for-postgres
network: host network: host
@@ -25,7 +25,7 @@ services:
context: ../images/plain context: ../images/plain
network: host network: host
depends_on: depends_on:
- automatisch - main
environment: environment:
- APP_ENV=production - APP_ENV=production
- REDIS_HOST=redis - REDIS_HOST=redis

View File

@@ -2,10 +2,4 @@
FROM node:16 FROM node:16
WORKDIR /automatisch WORKDIR /automatisch
# npm registry for dev purposes RUN yarn global add @automatisch/cli@0.1.3
RUN npm config set fetch-retry-maxtimeout 5000
RUN npm config set fetch-retry-mintimeout 3000
RUN npm set registry http://localhost:5000
# npm registry for dev purposes
RUN yarn global add @automatisch/cli

View File

@@ -5,17 +5,11 @@ WORKDIR /automatisch
RUN apt-get update && apt-get install -y postgresql-client RUN apt-get update && apt-get install -y postgresql-client
COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh
# npm registry for dev purposes
RUN npm config set fetch-retry-maxtimeout 5000
RUN npm config set fetch-retry-mintimeout 3000
RUN npm set registry http://localhost:5000
# npm registry for dev purposes
RUN mkdir -p /automatisch/storage RUN mkdir -p /automatisch/storage
RUN touch /automatisch/storage/.env RUN touch /automatisch/storage/.env
RUN echo "ENCRYPTION_KEY=$(openssl rand -base64 36)" >> /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 echo "APP_SECRET_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
RUN yarn global add @automatisch/cli RUN yarn global add @automatisch/cli@0.1.3
EXPOSE 3000 EXPOSE 3000
CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
{ {
"name": "@automatisch/backend", "name": "@automatisch/backend",
"version": "0.1.0", "version": "0.1.3",
"description": "> TODO: description", "license": "AGPL-3.0",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": { "scripts": {
"dev": "ts-node-dev src/server.ts", "dev": "ts-node-dev src/server.ts",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts", "worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
@@ -17,10 +18,11 @@
"db:rollback": "knex migrate:rollback", "db:rollback": "knex migrate:rollback",
"db:migrate": "knex migrate:latest", "db:migrate": "knex migrate:latest",
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist", "copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
"prepack": "yarn build" "prepack": "yarn build",
"prebuild": "rm -rf ./dist"
}, },
"dependencies": { "dependencies": {
"@automatisch/web": "0.1.0", "@automatisch/web": "^0.1.3",
"@bull-board/express": "^3.10.1", "@bull-board/express": "^3.10.1",
"@gitbeaker/node": "^35.6.0", "@gitbeaker/node": "^35.6.0",
"@graphql-tools/graphql-file-loader": "^7.3.4", "@graphql-tools/graphql-file-loader": "^7.3.4",
@@ -98,7 +100,7 @@
"url": "https://github.com/automatisch/automatisch/issues" "url": "https://github.com/automatisch/automatisch/issues"
}, },
"devDependencies": { "devDependencies": {
"@automatisch/types": "0.1.0", "@automatisch/types": "^0.1.3",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8", "@types/bull": "^3.15.8",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
@@ -128,5 +130,8 @@
"require": [ "require": [
"ts-node/register" "ts-node/register"
] ]
},
"publishConfig": {
"access": "public"
} }
} }

View File

@@ -4,19 +4,19 @@ import type {
IField, IField,
IJSONObject, IJSONObject,
} from '@automatisch/types'; } from '@automatisch/types';
import HttpClient from '../../helpers/http-client'; import createHttpClient, { IHttpClient } from '../../helpers/http-client';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
export default class Authentication implements IAuthentication { export default class Authentication implements IAuthentication {
appData: IApp; appData: IApp;
connectionData: IJSONObject; connectionData: IJSONObject;
scopes: string[] = ['read:org', 'repo', 'user']; scopes: string[] = ['read:org', 'repo', 'user'];
client: HttpClient; client: IHttpClient;
constructor(appData: IApp, connectionData: IJSONObject) { constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData; this.connectionData = connectionData;
this.appData = appData; this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://github.com' }); this.client = createHttpClient({ baseURL: 'https://github.com' });
} }
get oauthRedirectUrl(): string { get oauthRedirectUrl(): string {

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

View File

@@ -1,608 +0,0 @@
{
"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,
"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,15 +0,0 @@
import SendMessageToChannel from './actions/send-message-to-channel';
import FindMessage from './actions/find-message';
import SlackClient from './client';
export default class Actions {
client: SlackClient;
sendMessageToChannel: SendMessageToChannel;
findMessage: FindMessage;
constructor(client: SlackClient) {
this.client = client;
this.sendMessageToChannel = new SendMessageToChannel(client);
this.findMessage = new FindMessage(client);
}
}

View File

@@ -1,26 +0,0 @@
import SlackClient from '../client';
export default class FindMessage {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const parameters = this.client.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 this.client.findMessages.run(
query,
sortBy,
sortDirection,
count,
);
return messages;
}
}

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,18 +0,0 @@
import SlackClient from '../client';
export default class SendMessageToChannel {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const channelId = this.client.step.parameters.channel as string;
const text = this.client.step.parameters.message as string;
const message = await this.client.postMessageToChannel.run(channelId, text);
return message;
}
}

View File

@@ -1,7 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-label="Slack" role="img" aria-label="Slack" role="img"
viewBox="0 0 512 512"><rect viewBox="0 0 512 512"><rect
width="512" height="512" width="512" height="512"
rx="15%" 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,36 +0,0 @@
import type { IAuthentication, IJSONObject } from '@automatisch/types';
import SlackClient from './client';
export default class Authentication implements IAuthentication {
client: SlackClient;
static requestOptions: IJSONObject = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
constructor(client: SlackClient) {
this.client = client;
}
async verifyCredentials() {
const { bot_id: botId, user: screenName } =
await this.client.verifyAccessToken.run();
return {
botId,
screenName,
token: this.client.connection.formattedData.accessToken,
};
}
async isStillVerified() {
try {
await this.client.verifyAccessToken.run();
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -1,44 +0,0 @@
import SlackClient from '../index';
export default class FindMessages {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run(query: string, sortBy: string, sortDirection: string, count = 1) {
const headers = {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
};
const params = {
query,
sort: sortBy,
sort_dir: sortDirection,
count,
};
const response = await this.client.httpClient.get('/search.messages', {
headers,
params,
});
const data = response.data;
if (!data.ok) {
if (data.error === 'missing_scope') {
throw new Error(
`Error occured while finding messages; ${data.error}: ${data.needed}`
);
}
throw new Error(`Error occured while finding messages; ${data.error}`);
}
const messages = data.messages.matches;
const message = messages?.[0];
return message;
}
}

View File

@@ -1,34 +0,0 @@
import SlackClient from '../index';
export default class PostMessageToChannel {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run(channelId: string, text: string) {
const headers = {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
};
const params = {
channel: channelId,
text,
};
const response = await this.client.httpClient.post(
'/chat.postMessage',
params,
{ headers }
);
if (response.data.ok === 'false') {
throw new Error(
`Error occured while posting a message to channel: ${response.data.error}`
);
}
return response.data.message;
}
}

View File

@@ -1,35 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import qs from 'qs';
import SlackClient from '../index';
export default class VerifyAccessToken {
client: SlackClient;
static requestOptions: IJSONObject = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const response = await this.client.httpClient.post(
'/auth.test',
qs.stringify({
token: this.client.connection.formattedData.accessToken,
}),
VerifyAccessToken.requestOptions
);
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)`
);
}
return response.data;
}
}

View File

@@ -1,29 +0,0 @@
import { IFlow, IStep, IConnection } from '@automatisch/types';
import HttpClient from '../../../helpers/http-client';
import VerifyAccessToken from './endpoints/verify-access-token';
import PostMessageToChannel from './endpoints/post-message-to-channel';
import FindMessages from './endpoints/find-messages';
export default class SlackClient {
flow: IFlow;
step: IStep;
connection: IConnection;
httpClient: HttpClient;
verifyAccessToken: VerifyAccessToken;
postMessageToChannel: PostMessageToChannel;
findMessages: FindMessages;
static baseUrl = 'https://slack.com/api';
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.connection = connection;
this.flow = flow;
this.step = step;
this.httpClient = new HttpClient({ baseURL: SlackClient.baseUrl });
this.verifyAccessToken = new VerifyAccessToken(this);
this.postMessageToChannel = new PostMessageToChannel(this);
this.findMessages = new FindMessages(this);
}
}

View File

@@ -1,12 +0,0 @@
import ListChannels from './data/list-channels';
import SlackClient from './client';
export default class Data {
client: SlackClient;
listChannels: ListChannels;
constructor(client: SlackClient) {
this.client = client;
this.listChannels = new ListChannels(client);
}
}

View File

@@ -1,31 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import SlackClient from '../client';
export default class ListChannels {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const response = await this.client.httpClient.get('/conversations.list', {
headers: {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
},
});
if (response.data.ok === 'false') {
throw new Error(
`Error occured while fetching slack channels: ${response.data.error}`
);
}
return response.data.channels.map((channel: IJSONObject) => {
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,30 +1,8 @@
import { export default {
IService, name: 'Slack',
IAuthentication, key: 'slack',
IConnection, iconUrl: '{BASE_URL}/apps/slack/assets/favicon.svg',
IFlow, authDocUrl: 'https://automatisch.io/docs/connections/slack',
IStep, supportsConnections: true,
} from '@automatisch/types'; baseUrl: 'https://slack.com/api',
import Authentication from './authentication'; };
import Triggers from './triggers';
import Actions from './actions';
import Data from './data';
import SlackClient from './client';
export default class Slack implements IService {
client: SlackClient;
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
data: Data;
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.client = new SlackClient(connection, flow, step);
this.authenticationClient = new Authentication(this.client);
// this.triggers = new Triggers(this.client);
this.actions = new Actions(this.client);
this.data = new Data(this.client);
}
}

View File

@@ -1,277 +0,0 @@
{
"name": "Slack",
"key": "slack",
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/slack",
"authDocUrl": "https://automatisch.io/docs/connections/slack",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"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}"
}
]
}
],
"triggers": [
{
"name": "New message posted to a channel",
"key": "newMessageToChannel",
"pollInterval": 15,
"description": "Triggers when a new message is posted to a channel",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
},
{
"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": "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"
}
]
},
{
"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"
}
]
}
]
}

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

@@ -1,12 +0,0 @@
import TwitterClient from './client';
import CreateTweet from './actions/create-tweet';
export default class Actions {
client: TwitterClient;
createTweet: CreateTweet;
constructor(client: TwitterClient) {
this.client = client;
this.createTweet = new CreateTweet(client);
}
}

View File

@@ -1,17 +0,0 @@
import TwitterClient from '../client';
export default class CreateTweet {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
const tweet = await this.client.createTweet.run(
this.client.step.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"> <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"/> <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"/> <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,51 +0,0 @@
import type { IAuthentication, IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from './client';
export default class Authentication implements IAuthentication {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async createAuthData() {
const appFields = this.client.connection.appData.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;
const response = await this.client.oauthRequestToken.run(callbackUrl);
const responseData = Object.fromEntries(new URLSearchParams(response.data));
return {
url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
};
}
async verifyCredentials() {
const response = await this.client.verifyAccessToken.run();
const responseData = Object.fromEntries(new URLSearchParams(response.data));
return {
consumerKey: this.client.connection.formattedData.consumerKey as string,
consumerSecret: this.client.connection.formattedData
.consumerSecret as string,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
userId: responseData.user_id,
screenName: responseData.screen_name,
};
}
async isStillVerified() {
try {
await this.client.getCurrentUser.run();
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -1,40 +0,0 @@
import TwitterClient from '../index';
export default class CreateTweet {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(text: string) {
try {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestData = {
url: `${TwitterClient.baseUrl}/2/tweets`,
method: 'POST',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.post(
`/2/tweets`,
{ text },
{ headers: { ...authHeader } }
);
const tweet = response.data.data;
return tweet;
} catch (error) {
const errorMessage = error.response.data.detail;
throw new Error(`Error occured while creating a tweet: ${errorMessage}`);
}
}
}

View File

@@ -1,35 +0,0 @@
import TwitterClient from '../index';
export default class GetCurrentUser {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestPath = '/2/users/me';
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
const currentUser = response.data.data;
return currentUser;
}
}

View File

@@ -1,45 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import TwitterClient from '../index';
export default class GetUserByUsername {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(username: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestPath = `/2/users/by/username/${username}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
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;
}
}

View File

@@ -1,70 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
export default class GetUserFollowers {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(userId: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const followers: IJSONObject[] = [];
do {
const params: IJSONObject = {
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${userId}/followers${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
followers.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
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}`
);
}
return followers;
}
}

View File

@@ -1,71 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
export default class GetUserTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(userId: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const tweets: IJSONObject[] = [];
do {
const params: IJSONObject = {
since_id: lastInternalId,
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${userId}/tweets${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
tweets.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
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}`
);
}
return tweets;
}
}

View File

@@ -1,42 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import TwitterClient from '../index';
export default class OAuthRequestToken {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(callbackUrl: string) {
try {
const requestData = {
url: `${TwitterClient.baseUrl}/oauth/request_token`,
method: 'POST',
data: { oauth_callback: callbackUrl },
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData)
);
const response = await this.client.httpClient.post(
`/oauth/request_token`,
null,
{
headers: { ...authHeader },
}
);
return response;
} 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

@@ -1,73 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
import qs from 'qs';
export default class SearchTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(searchTerm: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const tweets: IJSONObject[] = [];
do {
const params: IJSONObject = {
query: searchTerm,
since_id: 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()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
tweets.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
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}`
);
}
return tweets;
}
}

View File

@@ -1,20 +0,0 @@
import TwitterClient from '../index';
export default class VerifyAccessToken {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
try {
return await this.client.httpClient.post(
`/oauth/access_token?oauth_verifier=${this.client.connection.formattedData.oauthVerifier}&oauth_token=${this.client.connection.formattedData.accessToken}`,
null
);
} catch (error) {
throw new Error(error.response.data);
}
}
}

View File

@@ -1,64 +0,0 @@
import { IFlow, IStep, IConnection } from '@automatisch/types';
import OAuth from 'oauth-1.0a';
import crypto from 'crypto';
import HttpClient from '../../../helpers/http-client';
import OAuthRequestToken from './endpoints/oauth-request-token';
import VerifyAccessToken from './endpoints/verify-access-token';
import GetCurrentUser from './endpoints/get-current-user';
import GetUserByUsername from './endpoints/get-user-by-username';
import GetUserTweets from './endpoints/get-user-tweets';
import CreateTweet from './endpoints/create-tweet';
import SearchTweets from './endpoints/search-tweets';
import GetUserFollowers from './endpoints/get-user-followers';
export default class TwitterClient {
flow: IFlow;
step: IStep;
connection: IConnection;
oauthClient: OAuth;
httpClient: HttpClient;
oauthRequestToken: OAuthRequestToken;
verifyAccessToken: VerifyAccessToken;
getCurrentUser: GetCurrentUser;
getUserByUsername: GetUserByUsername;
getUserTweets: GetUserTweets;
createTweet: CreateTweet;
searchTweets: SearchTweets;
getUserFollowers: GetUserFollowers;
static baseUrl = 'https://api.twitter.com';
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.connection = connection;
this.flow = flow;
this.step = step;
this.httpClient = new HttpClient({ baseURL: TwitterClient.baseUrl });
const consumerData = {
key: this.connection.formattedData.consumerKey as string,
secret: this.connection.formattedData.consumerSecret as string,
};
this.oauthClient = new OAuth({
consumer: consumerData,
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
},
});
this.oauthRequestToken = new OAuthRequestToken(this);
this.verifyAccessToken = new VerifyAccessToken(this);
this.getCurrentUser = new GetCurrentUser(this);
this.getUserByUsername = new GetUserByUsername(this);
this.getUserTweets = new GetUserTweets(this);
this.createTweet = new CreateTweet(this);
this.searchTweets = new SearchTweets(this);
this.getUserFollowers = new GetUserFollowers(this);
}
}

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,27 +1,8 @@
import { export default {
IService, name: 'Twitter',
IAuthentication, key: 'twitter',
IFlow, iconUrl: '{BASE_URL}/apps/twitter/assets/favicon.svg',
IStep, authDocUrl: 'https://automatisch.io/docs/connections/twitter',
IConnection, supportsConnections: true,
} from '@automatisch/types'; baseUrl: 'https://api.twitter.com',
import Authentication from './authentication'; };
import Triggers from './triggers';
import Actions from './actions';
import TwitterClient from './client';
export default class Twitter implements IService {
client: TwitterClient;
authenticationClient: IAuthentication;
triggers: Triggers;
actions: Actions;
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
this.client = new TwitterClient(connection, flow, step);
this.authenticationClient = new Authentication(this.client);
this.triggers = new Triggers(this.client);
this.actions = new Actions(this.client);
}
}

View File

@@ -1,338 +0,0 @@
{
"name": "Twitter",
"key": "twitter",
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
"docUrl": "https://automatisch.io/docs/twitter",
"authDocUrl": "https://automatisch.io/docs/connections/twitter",
"primaryColor": "2DAAE1",
"supportsConnections": true,
"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}"
}
]
}
],
"triggers": [
{
"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"
}
]
},
{
"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"
}
]
},
{
"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"
}
]
},
{
"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"
}
]
}
],
"actions": [
{
"name": "Create Tweet",
"key": "createTweet",
"description": "Will create a tweet.",
"substeps": [
{
"key": "chooseConnection",
"name": "Choose connection"
},
{
"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,21 +0,0 @@
import TwitterClient from './client';
import UserTweets from './triggers/user-tweets';
import SearchTweets from './triggers/search-tweets';
import MyTweets from './triggers/my-tweets';
import MyFollowers from './triggers/my-followers';
export default class Triggers {
client: TwitterClient;
userTweets: UserTweets;
searchTweets: SearchTweets;
myTweets: MyTweets;
myFollowers: MyFollowers;
constructor(client: TwitterClient) {
this.client = client;
this.userTweets = new UserTweets(client);
this.searchTweets = new SearchTweets(client);
this.myTweets = new MyTweets(client);
this.myFollowers = new MyFollowers(client);
}
}

View File

@@ -1,28 +0,0 @@
import TwitterClient from '../client';
export default class MyFollowers {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getFollowers(lastInternalId);
}
async testRun() {
return this.getFollowers();
}
async getFollowers(lastInternalId?: string) {
const { username } = await this.client.getCurrentUser.run();
const user = await this.client.getUserByUsername.run(username as string);
const tweets = await this.client.getUserFollowers.run(
user.id,
lastInternalId
);
return tweets;
}
}

View File

@@ -1,25 +0,0 @@
import TwitterClient from '../client';
export default class MyTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const { username } = await this.client.getCurrentUser.run();
const user = await this.client.getUserByUsername.run(username as string);
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
return tweets;
}
}

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,26 +0,0 @@
import TwitterClient from '../client';
export default class SearchTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const tweets = await this.client.searchTweets.run(
this.client.step.parameters.searchTerm as string,
lastInternalId
);
return tweets;
}
}

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

@@ -1,27 +0,0 @@
import TwitterClient from '../client';
export default class UserTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const user = await this.client.getUserByUsername.run(
this.client.step.parameters.username as string
);
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
return tweets;
}
}

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,12 +5,12 @@ import type {
IJSONObject, IJSONObject,
} from '@automatisch/types'; } from '@automatisch/types';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
import HttpClient from '../../helpers/http-client'; import createHttpClient, { IHttpClient } from '../../helpers/http-client';
export default class Authentication implements IAuthentication { export default class Authentication implements IAuthentication {
appData: IApp; appData: IApp;
connectionData: IJSONObject; connectionData: IJSONObject;
client: HttpClient; client: IHttpClient;
scope: string[] = [ scope: string[] = [
'forms:read', 'forms:read',
@@ -25,7 +25,7 @@ export default class Authentication implements IAuthentication {
constructor(appData: IApp, connectionData: IJSONObject) { constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData; this.connectionData = connectionData;
this.appData = appData; this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://api.typeform.com' }); this.client = createHttpClient({ baseURL: 'https://api.typeform.com' });
} }
get oauthRedirectUrl() { get oauthRedirectUrl() {

View File

@@ -22,6 +22,7 @@ type AppConfig = {
redisHost: string; redisHost: string;
redisPort: number; redisPort: number;
enableBullMQDashboard: boolean; enableBullMQDashboard: boolean;
telemetryEnabled: boolean;
}; };
const host = process.env.HOST || 'localhost'; const host = process.env.HOST || 'localhost';
@@ -62,6 +63,7 @@ const appConfig: AppConfig = {
process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false, process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false,
baseUrl, baseUrl,
webAppUrl, webAppUrl,
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
}; };
if (!appConfig.encryptionKey) { if (!appConfig.encryptionKey) {

View File

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

View File

@@ -13,7 +13,7 @@ const createConnection = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
App.findOneByKey(params.input.key); await App.findOneByKey(params.input.key);
return await context.currentUser.$relatedQuery('connections').insert({ return await context.currentUser.$relatedQuery('connections').insert({
key: params.input.key, key: params.input.key,

View File

@@ -1,4 +1,6 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import Execution from '../../models/execution';
import ExecutionStep from '../../models/execution-step';
type Params = { type Params = {
input: { input: {
@@ -11,14 +13,23 @@ const deleteFlow = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
await context.currentUser const flow = await context.currentUser
.$relatedQuery('flows') .$relatedQuery('flows')
.delete()
.findOne({ .findOne({
id: params.input.id, id: params.input.id,
}) })
.throwIfNotFound(); .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; return;
}; };

View File

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

View File

@@ -24,7 +24,7 @@ const executeFlow = async (
const flow = await untilStep.$relatedQuery('flow'); const flow = await untilStep.$relatedQuery('flow');
const data = await new Processor(flow, { const executionStep = await new Processor(flow, {
untilStep, untilStep,
testRun: true, testRun: true,
}).run(); }).run();
@@ -33,7 +33,11 @@ const executeFlow = async (
status: 'completed', status: 'completed',
}); });
return { data, step: untilStep }; if (executionStep.errorDetails) {
throw new Error(JSON.stringify(executionStep.errorDetails));
}
return { data: executionStep.dataOut, step: untilStep };
}; };
export default executeFlow; export default executeFlow;

View File

@@ -33,7 +33,7 @@ const updateFlowStatus = async (
const triggerStep = await flow.getTriggerStep(); const triggerStep = await flow.getTriggerStep();
const trigger = await triggerStep.getTrigger(); const trigger = await triggerStep.getTrigger();
const interval = trigger.interval; const interval = trigger?.getInterval(triggerStep.parameters);
const repeatOptions = { const repeatOptions = {
cron: interval || EVERY_15_MINUTES_CRON, cron: interval || EVERY_15_MINUTES_CRON,
}; };

View File

@@ -1,3 +1,4 @@
import { IJSONObject } from '@automatisch/types';
import Step from '../../models/step'; import Step from '../../models/step';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
@@ -6,7 +7,7 @@ type Params = {
id: string; id: string;
key: string; key: string;
appKey: string; appKey: string;
parameters: Record<string, unknown>; parameters: IJSONObject;
flow: { flow: {
id: string; id: string;
}; };

View File

@@ -1,5 +1,6 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import App from '../../models/app'; import App from '../../models/app';
import globalVariable from '../../helpers/global-variable';
type Params = { type Params = {
input: { input: {
@@ -19,18 +20,11 @@ const verifyConnection = async (
}) })
.throwIfNotFound(); .throwIfNotFound();
const appClass = (await import(`../../apps/${connection.key}`)).default; const app = await App.findOneByKey(connection.key);
const app = App.findOneByKey(connection.key); const $ = await globalVariable(connection, app);
await app.auth.verifyCredentials($);
const appInstance = new appClass(connection);
const verifiedCredentials =
await appInstance.authenticationClient.verifyCredentials();
connection = await connection.$query().patchAndFetch({ connection = await connection.$query().patchAndFetch({
formattedData: {
...connection.formattedData,
...verifiedCredentials,
},
verified: true, verified: true,
draft: false, draft: false,
}); });

View File

@@ -6,7 +6,7 @@ type Params = {
}; };
const getApp = async (_parent: unknown, params: Params, context: Context) => { const getApp = async (_parent: unknown, params: Params, context: Context) => {
const app = App.findOneByKey(params.key); const app = await App.findOneByKey(params.key);
if (context.currentUser) { if (context.currentUser) {
const connections = await context.currentUser const connections = await context.currentUser

View File

@@ -6,8 +6,8 @@ type Params = {
onlyWithTriggers: boolean; onlyWithTriggers: boolean;
}; };
const getApps = (_parent: unknown, params: Params) => { const getApps = async (_parent: unknown, params: Params) => {
const apps = App.findAll(params.name); const apps = await App.findAll(params.name);
if (params.onlyWithTriggers) { if (params.onlyWithTriggers) {
return apps.filter((app: IApp) => app.triggers?.length); return apps.filter((app: IApp) => app.triggers?.length);

View File

@@ -11,7 +11,7 @@ const getConnectedApps = async (
params: Params, params: Params,
context: Context context: Context
) => { ) => {
let apps = App.findAll(params.name); let apps = await App.findAll(params.name);
const connections = await context.currentUser const connections = await context.currentUser
.$relatedQuery('connections') .$relatedQuery('connections')

View File

@@ -1,5 +1,7 @@
import { IJSONObject } from '@automatisch/types'; import { IData, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import App from '../../models/app';
import globalVariable from '../../helpers/global-variable';
type Params = { type Params = {
stepId: string; stepId: string;
@@ -22,13 +24,18 @@ const getData = async (_parent: unknown, params: Params, context: Context) => {
if (!connection || !step.appKey) return null; if (!connection || !step.appKey) return null;
const AppClass = (await import(`../../apps/${step.appKey}`)).default; const app = await App.findOneByKey(step.appKey);
const appInstance = new AppClass(connection, step.flow, step); const $ = await globalVariable(connection, app, step.flow, step);
const command = appInstance.data[params.key]; const command = app.data.find((data: IData) => data.key === params.key);
const fetchedData = await command.run();
return fetchedData; const fetchedData = await command.run($);
if (fetchedData.error) {
throw new Error(JSON.stringify(fetchedData.error));
}
return fetchedData.data;
}; };
export default getData; export default getData;

View File

@@ -34,6 +34,7 @@ const getFlows = async (_parent: unknown, params: Params, context: Context) => {
} }
}) })
.groupBy('flows.id') .groupBy('flows.id')
.orderBy('active', 'desc')
.orderBy('updated_at', 'desc'); .orderBy('updated_at', 'desc');
return paginate(flowsQuery, params.limit, params.offset); return paginate(flowsQuery, params.limit, params.offset);

View File

@@ -1,4 +1,6 @@
import Context from '../../types/express/context'; import Context from '../../types/express/context';
import ExecutionStep from '../../models/execution-step';
import { ref } from 'objection';
type Params = { type Params = {
stepId: string; stepId: string;
@@ -19,11 +21,15 @@ const getStepWithTestExecutions = async (
.withGraphJoined('executionSteps') .withGraphJoined('executionSteps')
.where('flow_id', '=', step.flowId) .where('flow_id', '=', step.flowId)
.andWhere('position', '<', step.position) .andWhere('position', '<', step.position)
.distinctOn('executionSteps.step_id') .andWhere(
.orderBy([ 'executionSteps.created_at',
'executionSteps.step_id', '=',
{ column: 'executionSteps.created_at', order: 'desc' }, ExecutionStep.query()
]); .max('created_at')
.where('step_id', '=', ref('steps.id'))
.andWhere('status', 'success')
)
.orderBy('steps.position', 'asc');
return previousStepsWithCurrentStep; return previousStepsWithCurrentStep;
}; };

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