Compare commits
354 Commits
new-find-p
...
v0.1.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
dcd2986a12 | ||
![]() |
c9061b24a0 | ||
![]() |
a3dc7d2cde | ||
![]() |
cf90e7cfd6 | ||
![]() |
bbc9ea9571 | ||
![]() |
577abde2e9 | ||
![]() |
8e972fd875 | ||
![]() |
c7b290a380 | ||
![]() |
3d14208175 | ||
![]() |
04ad03cfd9 | ||
![]() |
48e9053124 | ||
![]() |
bd5aae3469 | ||
![]() |
2d3e307189 | ||
![]() |
c79c82cac7 | ||
![]() |
a80cd106bf | ||
![]() |
e1d953c855 | ||
![]() |
096837bf22 | ||
![]() |
ac4be28a03 | ||
![]() |
42ffd907ae | ||
![]() |
11ef2ac4bc | ||
![]() |
2862953136 | ||
![]() |
c958abdfcf | ||
![]() |
050f392dd1 | ||
![]() |
80b25694b5 | ||
![]() |
e462d1f7ea | ||
![]() |
e7b47f5c98 | ||
![]() |
89d7359060 | ||
![]() |
8a09408cf5 | ||
![]() |
45c0995d9d | ||
![]() |
0f3d1f0173 | ||
![]() |
a83983fe06 | ||
![]() |
b3937af636 | ||
![]() |
5037e67857 | ||
![]() |
ae76f7100c | ||
![]() |
0825eb36e4 | ||
![]() |
a58a59788d | ||
![]() |
77624acc01 | ||
![]() |
63ffd1f720 | ||
![]() |
8308265a62 | ||
![]() |
dc0e03245f | ||
![]() |
67cb57e7ac | ||
![]() |
4d5be952ce | ||
![]() |
0abab06124 | ||
![]() |
048689d28f | ||
![]() |
0be4b74d0e | ||
![]() |
26ed5cc794 | ||
![]() |
d236494c0b | ||
![]() |
52e4e09e85 | ||
![]() |
3b8a12810c | ||
![]() |
e5bd4228b2 | ||
![]() |
8e60f3dfd7 | ||
![]() |
18f315d26c | ||
![]() |
fca140e3c9 | ||
![]() |
330902ccb3 | ||
![]() |
932994622e | ||
![]() |
37a60e3105 | ||
![]() |
bce11819ff | ||
![]() |
d97fab28e3 | ||
![]() |
620629ff8b | ||
![]() |
e149d47135 | ||
![]() |
45cce9956b | ||
![]() |
dd003845af | ||
![]() |
93b31c10ae | ||
![]() |
c9056516a1 | ||
![]() |
953a031ba6 | ||
![]() |
e2f04441c5 | ||
![]() |
8826125845 | ||
![]() |
d0bcf997fb | ||
![]() |
9429031a61 | ||
![]() |
556cdc17bd | ||
![]() |
db052baea3 | ||
![]() |
e858fc2df8 | ||
![]() |
d5d239937a | ||
![]() |
68fb34491c | ||
![]() |
6ab7265a04 | ||
![]() |
c13248fb44 | ||
![]() |
2bc5da885e | ||
![]() |
1bd7f7a1c5 | ||
![]() |
5e8884c3f4 | ||
![]() |
ed0d22dcd7 | ||
![]() |
7e960024dd | ||
![]() |
90f4c873bb | ||
![]() |
1e34ab1ec6 | ||
![]() |
4cf25a8781 | ||
![]() |
53264e0637 | ||
![]() |
7cb3c00709 | ||
![]() |
ef1619fafb | ||
![]() |
10b9d69ac8 | ||
![]() |
2223cd7491 | ||
![]() |
927a0983ee | ||
![]() |
bb752e2bce | ||
![]() |
121b99a526 | ||
![]() |
7d325f06f6 | ||
![]() |
a2d83f5f21 | ||
![]() |
85473f6dcf | ||
![]() |
bfb685047e | ||
![]() |
416649c8cd | ||
![]() |
aef77dde59 | ||
![]() |
e1354c15fc | ||
![]() |
b41fca7e6c | ||
![]() |
dc89da2420 | ||
![]() |
1a1e7e57fb | ||
![]() |
cc0c73f226 | ||
![]() |
3c46f747b5 | ||
![]() |
0f8e4613da | ||
![]() |
801052adcd | ||
![]() |
70af9981e7 | ||
![]() |
715c900d88 | ||
![]() |
8162ab4299 | ||
![]() |
a2eae0da6d | ||
![]() |
832876d711 | ||
![]() |
4d7ee3dcca | ||
![]() |
6a8c642b72 | ||
![]() |
fe200fb790 | ||
![]() |
601582002a | ||
![]() |
81349bfdb3 | ||
![]() |
ab71dd2974 | ||
![]() |
40dfbae70c | ||
![]() |
31613225af | ||
![]() |
09b3f96189 | ||
![]() |
cfef769604 | ||
![]() |
364e69600c | ||
![]() |
b57f0735c9 | ||
![]() |
58565fddae | ||
![]() |
4183884537 | ||
![]() |
8c12d2dc85 | ||
![]() |
b8970fc3f8 | ||
![]() |
dc8a32b0b4 | ||
![]() |
02deed3e3a | ||
![]() |
e50f641533 | ||
![]() |
bd754da1ed | ||
![]() |
23c568c87c | ||
![]() |
cc9b047300 | ||
![]() |
eb3ac23c01 | ||
![]() |
5358d6ce5d | ||
![]() |
c0eaab3254 | ||
![]() |
2a39435413 | ||
![]() |
01ea316c1f | ||
![]() |
b253040d0a | ||
![]() |
57748a3541 | ||
![]() |
486eb088cf | ||
![]() |
c07a741970 | ||
![]() |
d1c7d5ef70 | ||
![]() |
4b15dad5ea | ||
![]() |
577e3fc669 | ||
![]() |
b5c7c6d88b | ||
![]() |
d812bb431f | ||
![]() |
3593727d29 | ||
![]() |
75b536959e | ||
![]() |
29a9338ad7 | ||
![]() |
3755a3ce4c | ||
![]() |
ec0d0203ae | ||
![]() |
72e3a69bd9 | ||
![]() |
2c61fb0c8b | ||
![]() |
56352560a5 | ||
![]() |
2ac7c17514 | ||
![]() |
a5b3a68588 | ||
![]() |
10dcccf99f | ||
![]() |
d0922d85b3 | ||
![]() |
fc330e25cf | ||
![]() |
dc79d623be | ||
![]() |
91f5f1d628 | ||
![]() |
e0f055a375 | ||
![]() |
7f3098362a | ||
![]() |
26eee1bb63 | ||
![]() |
0246d48584 | ||
![]() |
92053ea25a | ||
![]() |
d12984f324 | ||
![]() |
0eb28ec1a5 | ||
![]() |
20f8cca07d | ||
![]() |
9613e142c9 | ||
![]() |
1cc752b5c7 | ||
![]() |
163629b990 | ||
![]() |
770e115be9 | ||
![]() |
0eb5f3d3e6 | ||
![]() |
6e9a9992c0 | ||
![]() |
827fef8a05 | ||
![]() |
b53fcdebe3 | ||
![]() |
6537a1c649 | ||
![]() |
abaad7cb82 | ||
![]() |
ba140f05d3 | ||
![]() |
81a444e056 | ||
![]() |
db2c3556de | ||
![]() |
9bd1447bcf | ||
![]() |
fda957b1f6 | ||
![]() |
29abf702bd | ||
![]() |
5ddb5ab6fa | ||
![]() |
e997aa6c81 | ||
![]() |
6271cedc25 | ||
![]() |
743b6d6587 | ||
![]() |
ee4303a676 | ||
![]() |
c361b9af8d | ||
![]() |
f66656fd4e | ||
![]() |
be141e55a9 | ||
![]() |
5ed3b9230e | ||
![]() |
44e3de8534 | ||
![]() |
cd6c5216ff | ||
![]() |
17010f9283 | ||
![]() |
5fb988ae2d | ||
![]() |
c4a3f19bba | ||
![]() |
347c9c9455 | ||
![]() |
731443ab7d | ||
![]() |
898ab41167 | ||
![]() |
cf4f5bd084 | ||
![]() |
c7e55fe3e0 | ||
![]() |
b737cf68ba | ||
![]() |
c95622affe | ||
![]() |
c1b637b284 | ||
![]() |
bb37299c5b | ||
![]() |
2e9d5fb2fc | ||
![]() |
2aeabd4be3 | ||
![]() |
a4a9d60d68 | ||
![]() |
dac9276b9e | ||
![]() |
38700256b0 | ||
![]() |
a5f391d2dc | ||
![]() |
77d7260698 | ||
![]() |
782b2bafaa | ||
![]() |
a35bee0bc9 | ||
![]() |
c7d5584cd9 | ||
![]() |
bb68b2dea1 | ||
![]() |
3b587de138 | ||
![]() |
c827ce4270 | ||
![]() |
c6c3cbb1d3 | ||
![]() |
ad97fae883 | ||
![]() |
a5b6e66e22 | ||
![]() |
5d7daa8886 | ||
![]() |
47ba42f9f8 | ||
![]() |
04f8a71244 | ||
![]() |
3148118784 | ||
![]() |
b7c4a63d2b | ||
![]() |
a4abd7e1cd | ||
![]() |
0b01a6386d | ||
![]() |
fa75f11eaf | ||
![]() |
a1b360a172 | ||
![]() |
e0a76ea918 | ||
![]() |
7990c68d65 | ||
![]() |
8c5d95796f | ||
![]() |
b901a396bf | ||
![]() |
fa92375e33 | ||
![]() |
04b3d93d5d | ||
![]() |
1f8bc9cfbd | ||
![]() |
71939f9163 | ||
![]() |
0ea8f3a01a | ||
![]() |
ffa49149c9 | ||
![]() |
40a35691fe | ||
![]() |
908a3126f1 | ||
![]() |
726707afe8 | ||
![]() |
3c926adeca | ||
![]() |
fc681d9ebc | ||
![]() |
364f53142c | ||
![]() |
2764aa2c06 | ||
![]() |
ca141b1076 | ||
![]() |
2e980664ac | ||
![]() |
03cfa8b904 | ||
![]() |
c5bb07a768 | ||
![]() |
3a1e8b4bbd | ||
![]() |
0e749936a6 | ||
![]() |
744b31aad6 | ||
![]() |
82bdf9d3b1 | ||
![]() |
8c59bd664e | ||
![]() |
15aaada3fe | ||
![]() |
0fdbe4d39b | ||
![]() |
280c7832ae | ||
![]() |
1f9dd6a3bc | ||
![]() |
e7f12f4a06 | ||
![]() |
5a24b9ec8a | ||
![]() |
1d8a72e03b | ||
![]() |
ef987eae36 | ||
![]() |
dc4899c240 | ||
![]() |
88a780f008 | ||
![]() |
6fc13f3707 | ||
![]() |
6f1b9b8fe7 | ||
![]() |
533d73d718 | ||
![]() |
63241d2438 | ||
![]() |
5a177b330a | ||
![]() |
ae8f701e5c | ||
![]() |
744c927b69 | ||
![]() |
6ca1b99947 | ||
![]() |
cdb018dcd2 | ||
![]() |
03efe3f0b3 | ||
![]() |
913a2773c1 | ||
![]() |
10c64167d7 | ||
![]() |
a624a8d8b5 | ||
![]() |
ff09a836b4 | ||
![]() |
e7c734c55e | ||
![]() |
5271af8b94 | ||
![]() |
df55d9fdd9 | ||
![]() |
c7ff9dc162 | ||
![]() |
f7ab2b667c | ||
![]() |
b89086d3b8 | ||
![]() |
448a1a49b4 | ||
![]() |
98d7969a1e | ||
![]() |
d513e03138 | ||
![]() |
a5367c3770 | ||
![]() |
58d5847eed | ||
![]() |
02b22740b2 | ||
![]() |
70d59c6c64 | ||
![]() |
095948e737 | ||
![]() |
e6cec355cc | ||
![]() |
351f152664 | ||
![]() |
04450b8793 | ||
![]() |
76612e81b2 | ||
![]() |
cb26948db6 | ||
![]() |
18cdd226bb | ||
![]() |
2199970e50 | ||
![]() |
8389c9fdad | ||
![]() |
0d06ec9a22 | ||
![]() |
ce56f166cc | ||
![]() |
a07938d517 | ||
![]() |
7d81a4bdd2 | ||
![]() |
c4bffe7f9d | ||
![]() |
0c2caccc7c | ||
![]() |
d3c9f7a491 | ||
![]() |
f09fa0fe7c | ||
![]() |
8f1fbf086f | ||
![]() |
272c666d23 | ||
![]() |
a1db89700b | ||
![]() |
311f0a747d | ||
![]() |
69298857a9 | ||
![]() |
c98680fa59 | ||
![]() |
5f357afcd6 | ||
![]() |
997775e54b | ||
![]() |
2cf79e27de | ||
![]() |
95d03e00da | ||
![]() |
c85aadf006 | ||
![]() |
e9ffb7ef82 | ||
![]() |
4237972c86 | ||
![]() |
b69009f8b6 | ||
![]() |
6fa6ee7a1b | ||
![]() |
7cf1bfffbc | ||
![]() |
903b0f52b9 | ||
![]() |
16e299a12d | ||
![]() |
e950d73742 | ||
![]() |
b44cccb972 | ||
![]() |
5d4ef1bbe8 | ||
![]() |
12a6912d97 | ||
![]() |
2740f8e23d | ||
![]() |
d0814477eb | ||
![]() |
cdb256390c | ||
![]() |
9895cc1488 | ||
![]() |
fad2495941 | ||
![]() |
f9fa7c4094 | ||
![]() |
a5538a07f1 | ||
![]() |
d1b46df78a | ||
![]() |
57186bf85d | ||
![]() |
cc1892a5cf | ||
![]() |
c59bb85793 | ||
![]() |
a88d208cd6 | ||
![]() |
91e209e961 | ||
![]() |
7c543ff8ed | ||
![]() |
9f56fd7c3b | ||
![]() |
3a63fc376d | ||
![]() |
445ed9239d | ||
![]() |
9d5060f902 |
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal 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 Mozilla’s 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
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Automatisch - Open Source Zapier Alternative
|
||||
|
||||

|
||||
|
||||
🧐 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).
|
47
docker/compose/docker-compose.yml
Normal file
47
docker/compose/docker-compose.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
version: "3.9"
|
||||
services:
|
||||
main:
|
||||
build:
|
||||
context: ../images/wait-for-postgres
|
||||
network: host
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- HOST=localhost
|
||||
- PROTOCOL=http
|
||||
- PORT=3000
|
||||
- APP_ENV=production
|
||||
- REDIS_HOST=redis
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_DATABASE=automatisch
|
||||
- POSTGRES_USERNAME=automatisch_user
|
||||
volumes:
|
||||
- automatisch_storage:/automatisch/storage
|
||||
worker:
|
||||
build:
|
||||
context: ../images/plain
|
||||
network: host
|
||||
depends_on:
|
||||
- main
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- REDIS_HOST=redis
|
||||
- POSTGRES_HOST=postgres
|
||||
- POSTGRES_DATABASE=automatisch
|
||||
- POSTGRES_USERNAME=automatisch_user
|
||||
command: automatisch start-worker --env-file /automatisch/storage/.env
|
||||
volumes:
|
||||
- automatisch_storage:/automatisch/storage
|
||||
postgres:
|
||||
image: "postgres:14.5"
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
POSTGRES_DB: automatisch
|
||||
POSTGRES_USER: automatisch_user
|
||||
redis:
|
||||
image: "redis:7.0.4"
|
||||
volumes:
|
||||
automatisch_storage:
|
5
docker/images/plain/Dockerfile
Normal file
5
docker/images/plain/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:16
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN yarn global add @automatisch/cli
|
15
docker/images/wait-for-postgres/Dockerfile
Normal file
15
docker/images/wait-for-postgres/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:16
|
||||
WORKDIR /automatisch
|
||||
|
||||
RUN apt-get update && apt-get install -y postgresql-client
|
||||
COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh
|
||||
|
||||
RUN mkdir -p /automatisch/storage
|
||||
RUN touch /automatisch/storage/.env
|
||||
RUN echo "ENCRYPTION_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
|
||||
RUN echo "APP_SECRET_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
|
||||
RUN yarn global add @automatisch/cli
|
||||
|
||||
EXPOSE 3000
|
||||
CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env
|
11
docker/images/wait-for-postgres/wait-for-postgres.sh
Normal file
11
docker/images/wait-for-postgres/wait-for-postgres.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
until psql -h "$POSTGRES_HOST" -U "$POSTGRES_USERNAME" -d "$POSTGRES_HOST" -c '\q'; do
|
||||
>&2 echo "Postgres is unavailable - sleeping"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
>&2 echo "Postgres is up - executing command"
|
||||
exec "$@"
|
@@ -2,16 +2,12 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"private": true,
|
||||
"version": "independent",
|
||||
"version": "0.1.2",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"command": {
|
||||
"publish": {
|
||||
"registry": "http://localhost:5000"
|
||||
},
|
||||
"add": {
|
||||
"exact": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@automatisch/root",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||
@@ -31,6 +32,6 @@
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "http://localhost:5000"
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
@@ -13,3 +13,4 @@ ENCRYPTION_KEY=sample-encryption-key
|
||||
APP_SECRET_KEY=sample-app-secret-key
|
||||
REDIS_PORT=6379
|
||||
REDIS_HOST=127.0.0.1
|
||||
ENABLE_BULLMQ_DASHBOARD=false
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# `backend`
|
||||
|
||||
> TODO: description
|
||||
The open source Zapier alternative. Build workflow automation without spending
|
||||
time and money.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@@ -12,8 +12,14 @@ export async function createUser(email = 'user@automatisch.io', password = 'samp
|
||||
};
|
||||
|
||||
try {
|
||||
const user = await User.query().insertAndFetch(userParams);
|
||||
logger.info(`User has been saved: ${user.email}`);
|
||||
const userCount = await User.query().resultSize();
|
||||
|
||||
if (userCount === 0) {
|
||||
const user = await User.query().insertAndFetch(userParams);
|
||||
logger.info(`User has been saved: ${user.email}`);
|
||||
} else {
|
||||
logger.info('No need to seed a user.');
|
||||
}
|
||||
} catch (err) {
|
||||
if ((err as any).nativeError.code !== UNIQUE_VIOLATION_CODE) {
|
||||
throw err;
|
||||
|
1
packages/backend/database-utils.d.ts
vendored
Normal file
1
packages/backend/database-utils.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dist/bin/database/utils';
|
2
packages/backend/database-utils.js
Normal file
2
packages/backend/database-utils.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* eslint-disable */
|
||||
module.exports = require('./dist/bin/database/utils');
|
3
packages/backend/database.d.ts
vendored
3
packages/backend/database.d.ts
vendored
@@ -1,2 +1 @@
|
||||
export * as utils from './dist/bin/database/utils';
|
||||
export * as database from './dist/src/config/database';
|
||||
export * from './dist/src/config/database';
|
||||
|
@@ -1,3 +1,2 @@
|
||||
/* eslint-disable */
|
||||
module.exports.utils = require('./dist/bin/database/utils');
|
||||
module.exports.database = require('./dist/src/config/database');
|
||||
module.exports = require('./dist/src/config/database');
|
||||
|
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "@automatisch/backend",
|
||||
"version": "0.1.0",
|
||||
"description": "> TODO: description",
|
||||
"version": "0.1.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec 'ts-node' src/server.ts --ext ts,json",
|
||||
"dev": "ts-node-dev src/server.ts",
|
||||
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
|
||||
"build": "tsc && yarn copy-statics",
|
||||
"build:watch": "nodemon --watch 'src/**/*.ts' --watch 'bin/**/*.ts' --exec yarn build --ext ts",
|
||||
@@ -16,15 +17,16 @@
|
||||
"db:migration:create": "knex migrate:make",
|
||||
"db:rollback": "knex migrate:rollback",
|
||||
"db:migrate": "knex migrate:latest",
|
||||
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist"
|
||||
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
|
||||
"prepack": "yarn build",
|
||||
"prebuild": "rm -rf ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@automatisch/web": "0.1.0",
|
||||
"@automatisch/web": "^0.1.2",
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@gitbeaker/node": "^35.6.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
"@graphql-tools/load": "^7.5.2",
|
||||
"@octokit/oauth-methods": "^1.2.6",
|
||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||
"@slack/bolt": "3.10.0",
|
||||
"@types/luxon": "^2.3.1",
|
||||
@@ -53,6 +55,7 @@
|
||||
"luxon": "2.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "6.7.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"objection": "^3.0.0",
|
||||
"octokit": "^1.7.1",
|
||||
"pg": "^8.7.1",
|
||||
@@ -75,14 +78,19 @@
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bin",
|
||||
"src",
|
||||
"server.js",
|
||||
"server.d.ts",
|
||||
"worker.js",
|
||||
"worker.d.ts",
|
||||
"logger.js",
|
||||
"logger.d.ts",
|
||||
"database.js",
|
||||
"database.d.ts"
|
||||
"database.d.ts",
|
||||
"database-utils.js",
|
||||
"database-utils.d.ts"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -92,7 +100,7 @@
|
||||
"url": "https://github.com/automatisch/automatisch/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@automatisch/types": "0.1.0",
|
||||
"@automatisch/types": "^0.1.2",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/bull": "^3.15.8",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -109,7 +117,8 @@
|
||||
"ava": "^3.15.0",
|
||||
"nodemon": "^2.0.13",
|
||||
"sinon": "^11.1.2",
|
||||
"ts-node": "^10.2.1"
|
||||
"ts-node": "^10.2.1",
|
||||
"ts-node-dev": "^1.1.8"
|
||||
},
|
||||
"ava": {
|
||||
"files": [
|
||||
@@ -121,5 +130,8 @@
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
]
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
|
@@ -15,13 +15,13 @@ import {
|
||||
} from './helpers/create-bull-board-handler';
|
||||
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
|
||||
|
||||
if (appConfig.appEnv === 'development') {
|
||||
if (appConfig.enableBullMQDashboard) {
|
||||
createBullBoardHandler(serverAdapter);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
if (appConfig.appEnv === 'development') {
|
||||
if (appConfig.enableBullMQDashboard) {
|
||||
injectBullBoardHandler(app, serverAdapter);
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/discord/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/discord",
|
||||
"primaryColor": "5865f2",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/firebase/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/firebase",
|
||||
"primaryColor": "ffca28",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/flickr/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/flickr",
|
||||
"primaryColor": "000000",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
@@ -222,8 +223,8 @@
|
||||
"description": "Triggers when you favorite a photo.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
@@ -237,8 +238,8 @@
|
||||
"description": "Triggers when you add a new photo in an album.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -275,8 +276,8 @@
|
||||
"description": "Triggers when you add a new photo.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
@@ -290,8 +291,8 @@
|
||||
"description": "Triggers when you create a new album.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
|
@@ -4,35 +4,19 @@ import type {
|
||||
IField,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
import {
|
||||
getWebFlowAuthorizationUrl,
|
||||
exchangeWebFlowCode,
|
||||
checkToken,
|
||||
} from '@octokit/oauth-methods';
|
||||
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
export default class Authentication implements IAuthentication {
|
||||
appData: IApp;
|
||||
connectionData: IJSONObject;
|
||||
scopes: string[] = [
|
||||
'read:org',
|
||||
'repo',
|
||||
'user',
|
||||
];
|
||||
client: {
|
||||
getWebFlowAuthorizationUrl: typeof getWebFlowAuthorizationUrl;
|
||||
exchangeWebFlowCode: typeof exchangeWebFlowCode;
|
||||
checkToken: typeof checkToken;
|
||||
};
|
||||
scopes: string[] = ['read:org', 'repo', 'user'];
|
||||
client: IHttpClient;
|
||||
|
||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||
this.connectionData = connectionData;
|
||||
this.appData = appData;
|
||||
|
||||
this.client = {
|
||||
getWebFlowAuthorizationUrl,
|
||||
exchangeWebFlowCode,
|
||||
checkToken,
|
||||
};
|
||||
this.client = createHttpClient({ baseURL: 'https://github.com' });
|
||||
}
|
||||
|
||||
get oauthRedirectUrl(): string {
|
||||
@@ -42,26 +26,28 @@ export default class Authentication implements IAuthentication {
|
||||
}
|
||||
|
||||
async createAuthData(): Promise<{ url: string }> {
|
||||
const { url } = await this.client.getWebFlowAuthorizationUrl({
|
||||
clientType: 'oauth-app',
|
||||
clientId: this.connectionData.consumerKey as string,
|
||||
redirectUrl: this.oauthRedirectUrl,
|
||||
scopes: this.scopes,
|
||||
const searchParams = new URLSearchParams({
|
||||
client_id: this.connectionData.consumerKey as string,
|
||||
redirect_uri: this.oauthRedirectUrl,
|
||||
scope: this.scopes.join(','),
|
||||
});
|
||||
|
||||
const url = `https://github.com/login/oauth/authorize?${searchParams.toString()}`;
|
||||
|
||||
return {
|
||||
url: url,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyCredentials() {
|
||||
const { data } = await this.client.exchangeWebFlowCode({
|
||||
clientType: 'oauth-app',
|
||||
clientId: this.connectionData.consumerKey as string,
|
||||
clientSecret: this.connectionData.consumerSecret as string,
|
||||
code: this.connectionData.oauthVerifier as string,
|
||||
const response = await this.client.post('/login/oauth/access_token', {
|
||||
client_id: this.connectionData.consumerKey,
|
||||
client_secret: this.connectionData.consumerSecret,
|
||||
code: this.connectionData.oauthVerifier,
|
||||
});
|
||||
|
||||
const data = Object.fromEntries(new URLSearchParams(response.data));
|
||||
|
||||
this.connectionData.accessToken = data.access_token;
|
||||
|
||||
const tokenInfo = await this.getTokenInfo();
|
||||
@@ -78,12 +64,23 @@ export default class Authentication implements IAuthentication {
|
||||
}
|
||||
|
||||
async getTokenInfo() {
|
||||
return this.client.checkToken({
|
||||
clientType: 'oauth-app',
|
||||
clientId: this.connectionData.consumerKey as string,
|
||||
clientSecret: this.connectionData.consumerSecret as string,
|
||||
token: this.connectionData.accessToken as string,
|
||||
});
|
||||
const basicAuthToken = Buffer.from(
|
||||
this.connectionData.consumerKey + ':' + this.connectionData.consumerSecret
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
Authorization: `Basic ${basicAuthToken}`,
|
||||
};
|
||||
|
||||
const body = {
|
||||
access_token: this.connectionData.accessToken,
|
||||
};
|
||||
|
||||
return await this.client.post(
|
||||
`https://api.github.com/applications/${this.connectionData.consumerKey}/token`,
|
||||
body,
|
||||
{ headers }
|
||||
);
|
||||
}
|
||||
|
||||
async isStillVerified() {
|
||||
|
@@ -1,13 +1,16 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import ListRepos from './data/list-repos';
|
||||
import ListBranches from './data/list-branches';
|
||||
import ListLabels from './data/list-labels';
|
||||
|
||||
export default class Data {
|
||||
listRepos: ListRepos;
|
||||
listBranches: ListBranches;
|
||||
listLabels: ListLabels;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.listRepos = new ListRepos(connectionData);
|
||||
this.listBranches = new ListBranches(connectionData, parameters);
|
||||
this.listLabels = new ListLabels(connectionData, parameters);
|
||||
}
|
||||
}
|
||||
|
36
packages/backend/src/apps/github/data/list-labels.ts
Normal file
36
packages/backend/src/apps/github/data/list-labels.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Octokit } from 'octokit';
|
||||
import type { IJSONObject } from '@automatisch/types';
|
||||
|
||||
import { assignOwnerAndRepo } from '../utils';
|
||||
|
||||
export default class ListLabels {
|
||||
client?: Octokit;
|
||||
repoOwner?: string;
|
||||
repo?: string;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters?: IJSONObject) {
|
||||
if (connectionData.accessToken) {
|
||||
this.client = new Octokit({
|
||||
auth: connectionData.accessToken as string,
|
||||
});
|
||||
}
|
||||
|
||||
assignOwnerAndRepo(this, parameters?.repo as string);
|
||||
}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
owner: this.repoOwner,
|
||||
repo: this.repo,
|
||||
};
|
||||
}
|
||||
|
||||
async run() {
|
||||
const labels = await this.client.paginate(this.client.rest.issues.listLabelsForRepo, this.options);
|
||||
|
||||
return labels.map((label) => ({
|
||||
value: label.name,
|
||||
name: label.name,
|
||||
}));
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/github/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/github",
|
||||
"primaryColor": "000000",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
@@ -19,26 +20,26 @@
|
||||
},
|
||||
{
|
||||
"key": "consumerKey",
|
||||
"label": "Consumer Key",
|
||||
"label": "Client ID",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"value": null,
|
||||
"placeholder": null,
|
||||
"description": null,
|
||||
"docUrl": "https://automatisch.io/docs/github#consumer-key",
|
||||
"docUrl": "https://automatisch.io/docs/github#client-id",
|
||||
"clickToCopy": false
|
||||
},
|
||||
{
|
||||
"key": "consumerSecret",
|
||||
"label": "Consumer Secret",
|
||||
"label": "Client Secret",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"value": null,
|
||||
"placeholder": null,
|
||||
"description": null,
|
||||
"docUrl": "https://automatisch.io/docs/github#consumer-secret",
|
||||
"docUrl": "https://automatisch.io/docs/github#client-secret",
|
||||
"clickToCopy": false
|
||||
}
|
||||
],
|
||||
@@ -222,8 +223,8 @@
|
||||
"description": "Triggers when a new repository is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
@@ -237,8 +238,8 @@
|
||||
"description": "Triggers when a new organization is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
@@ -252,8 +253,8 @@
|
||||
"description": "Triggers when a new branch is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -290,8 +291,8 @@
|
||||
"description": "Triggers when a new notification is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -329,8 +330,8 @@
|
||||
"description": "Triggers when a new pull request is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -367,8 +368,8 @@
|
||||
"description": "Triggers when a new watcher is added to a repo",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -405,8 +406,8 @@
|
||||
"description": "Triggers when a new milestone is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -443,8 +444,8 @@
|
||||
"description": "Triggers when a new commit comment is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -481,8 +482,8 @@
|
||||
"description": "Triggers when a new label is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -519,8 +520,8 @@
|
||||
"description": "Triggers when a new collaborator is added to a repo",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -557,8 +558,8 @@
|
||||
"description": "Triggers when a new release is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -595,8 +596,8 @@
|
||||
"description": "Triggers when a new commit is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
@@ -649,6 +650,98 @@
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "New issue",
|
||||
"key": "newIssue",
|
||||
"description": "Triggers when a new issue is created",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseConnection",
|
||||
"name": "Choose connection"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Repo",
|
||||
"key": "repo",
|
||||
"type": "dropdown",
|
||||
"required": false,
|
||||
"variables": false,
|
||||
"source": {
|
||||
"type": "query",
|
||||
"name": "getData",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"value": "listRepos"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Which types of issues should this trigger on?",
|
||||
"key": "issueType",
|
||||
"type": "dropdown",
|
||||
"description": "Defaults to any issue you can see.",
|
||||
"required": true,
|
||||
"variables": false,
|
||||
"value": "all",
|
||||
"options": [
|
||||
{
|
||||
"label": "Any issue you can see",
|
||||
"value": "all"
|
||||
},
|
||||
{
|
||||
"label": "Only issues assigned to you",
|
||||
"value": "assigned"
|
||||
},
|
||||
{
|
||||
"label": "Only issues created by you",
|
||||
"value": "created"
|
||||
},
|
||||
{
|
||||
"label": "Only issues you're mentioned in",
|
||||
"value": "mentioned"
|
||||
},
|
||||
{
|
||||
"label": "Only issues you're subscribed to",
|
||||
"value": "subscribed"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Label",
|
||||
"key": "label",
|
||||
"type": "dropdown",
|
||||
"description": "Only trigger on issues when this label is added.",
|
||||
"required": false,
|
||||
"variables": false,
|
||||
"dependsOn": ["parameters.repo"],
|
||||
"source": {
|
||||
"type": "query",
|
||||
"name": "getData",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"value": "listLabels"
|
||||
},
|
||||
{
|
||||
"name": "parameters.repo",
|
||||
"value": "{parameters.repo}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import NewCommitComment from './triggers/new-commit-comment';
|
||||
import NewLabel from './triggers/new-label';
|
||||
import NewCollaborator from './triggers/new-collaborator';
|
||||
import NewRelease from './triggers/new-release';
|
||||
import NewIssue from './triggers/new-issue';
|
||||
|
||||
export default class Triggers {
|
||||
newRepository: NewRepository;
|
||||
@@ -25,6 +26,7 @@ export default class Triggers {
|
||||
newLabel: NewLabel;
|
||||
newCollaborator: NewCollaborator;
|
||||
newRelease: NewRelease;
|
||||
newIssue: NewIssue;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.newRepository = new NewRepository(connectionData);
|
||||
@@ -39,5 +41,6 @@ export default class Triggers {
|
||||
this.newLabel = new NewLabel(connectionData, parameters);
|
||||
this.newCollaborator = new NewCollaborator(connectionData, parameters);
|
||||
this.newRelease = new NewRelease(connectionData, parameters);
|
||||
this.newIssue = new NewIssue(connectionData, parameters);
|
||||
}
|
||||
}
|
||||
|
88
packages/backend/src/apps/github/triggers/new-issue.ts
Normal file
88
packages/backend/src/apps/github/triggers/new-issue.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Octokit } from 'octokit';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
import { assignOwnerAndRepo } from '../utils';
|
||||
|
||||
export default class NewIssue {
|
||||
client?: Octokit;
|
||||
connectionData?: IJSONObject;
|
||||
repoOwner?: string;
|
||||
repo?: string;
|
||||
hasRepo?: boolean;
|
||||
label?: string;
|
||||
issueType?: string;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
if (connectionData.accessToken) {
|
||||
this.client = new Octokit({
|
||||
auth: connectionData.accessToken as string,
|
||||
});
|
||||
}
|
||||
|
||||
assignOwnerAndRepo(this, parameters?.repo as string);
|
||||
}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
labels: this.label,
|
||||
}
|
||||
}
|
||||
|
||||
async listRepoIssues(options = {}, paginate = false) {
|
||||
const listRepoIssues = this.client.rest.issues.listForRepo;
|
||||
|
||||
const extendedOptions = {
|
||||
...this.options,
|
||||
repo: this.repo,
|
||||
owner: this.repoOwner,
|
||||
filter: this.issueType,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (paginate) {
|
||||
return await this.client.paginate(listRepoIssues, extendedOptions);
|
||||
}
|
||||
|
||||
return (await listRepoIssues(extendedOptions)).data;
|
||||
}
|
||||
|
||||
async listIssues(options = {}, paginate = false) {
|
||||
const listIssues = this.client.rest.issues.listForAuthenticatedUser;
|
||||
|
||||
const extendedOptions = {
|
||||
...this.options,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (paginate) {
|
||||
return await this.client.paginate(listIssues, extendedOptions);
|
||||
}
|
||||
|
||||
return (await listIssues(extendedOptions)).data;
|
||||
}
|
||||
|
||||
async run(startTime: Date) {
|
||||
const options = {
|
||||
since: DateTime.fromJSDate(startTime).toISO(),
|
||||
};
|
||||
|
||||
if (this.hasRepo) {
|
||||
return await this.listRepoIssues(options, true);
|
||||
}
|
||||
|
||||
return await this.listIssues(options, true);
|
||||
}
|
||||
|
||||
async testRun() {
|
||||
const options = {
|
||||
per_page: 1,
|
||||
};
|
||||
|
||||
if (this.hasRepo) {
|
||||
return await this.listRepoIssues(options, false);
|
||||
}
|
||||
|
||||
return await this.listIssues(options, false);
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@ export default class NewNotification {
|
||||
connectionData?: IJSONObject;
|
||||
repoOwner?: string;
|
||||
repo?: string;
|
||||
hasRepo?: boolean;
|
||||
baseOptions = {
|
||||
all: true,
|
||||
participating: false,
|
||||
@@ -24,10 +25,6 @@ export default class NewNotification {
|
||||
assignOwnerAndRepo(this, parameters?.repo as string);
|
||||
}
|
||||
|
||||
get hasRepo() {
|
||||
return this.repoOwner && this.repo;
|
||||
}
|
||||
|
||||
async listRepoNotifications(options = {}, paginate = false) {
|
||||
const listRepoNotifications = this.client.rest.activity.listRepoNotificationsForAuthenticatedUser;
|
||||
|
||||
|
@@ -1,8 +1,9 @@
|
||||
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; }>(object: T, repoFullName: string): T {
|
||||
export function assignOwnerAndRepo<T extends { repoOwner?: string; repo?: string; hasRepo?: boolean; }>(object: T, repoFullName: string): T {
|
||||
if (object && repoFullName) {
|
||||
const [repoOwner, repo] = repoFullName.split('/');
|
||||
object.repoOwner = repoOwner;
|
||||
object.repo = repo;
|
||||
object.hasRepo = true;
|
||||
}
|
||||
|
||||
return object;
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/gitlab/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/gitlab",
|
||||
"primaryColor": "2DAAE1",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/postgresql/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/postgresql",
|
||||
"primaryColor": "2DAAE1",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "host",
|
||||
|
@@ -1,18 +0,0 @@
|
||||
import Triggers from './triggers';
|
||||
import {
|
||||
IService,
|
||||
IApp,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
|
||||
export default class Schedule implements IService {
|
||||
triggers: Triggers;
|
||||
|
||||
constructor(
|
||||
appData: IApp,
|
||||
connectionData: IJSONObject,
|
||||
parameters: IJSONObject
|
||||
) {
|
||||
this.triggers = new Triggers(connectionData, parameters);
|
||||
}
|
||||
}
|
@@ -1,606 +0,0 @@
|
||||
{
|
||||
"name": "Schedule",
|
||||
"key": "schedule",
|
||||
"iconUrl": "{BASE_URL}/apps/schedule/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/schedule",
|
||||
"primaryColor": "0059F7",
|
||||
"requiresAuthentication": false,
|
||||
"triggers": [
|
||||
{
|
||||
"name": "Every hour",
|
||||
"key": "everyHour",
|
||||
"description": "Triggers every hour.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Trigger on weekends?",
|
||||
"key": "triggersOnWeekend",
|
||||
"type": "dropdown",
|
||||
"description": "Should this flow trigger on Saturday and Sunday?",
|
||||
"required": true,
|
||||
"value": true,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Yes",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"label": "No",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Every day",
|
||||
"key": "everyDay",
|
||||
"description": "Triggers every day.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Trigger on weekends?",
|
||||
"key": "triggersOnWeekend",
|
||||
"type": "dropdown",
|
||||
"description": "Should this flow trigger on Saturday and Sunday?",
|
||||
"required": true,
|
||||
"value": true,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Yes",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"label": "No",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Time of day",
|
||||
"key": "hour",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"value": null,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "00:00",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"label": "01:00",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": "02:00",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": "03:00",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"label": "04:00",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"label": "05:00",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"label": "06:00",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"label": "07:00",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"label": "08:00",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"label": "09:00",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"label": "10:00",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"label": "11:00",
|
||||
"value": 11
|
||||
},
|
||||
{
|
||||
"label": "12:00",
|
||||
"value": 12
|
||||
},
|
||||
{
|
||||
"label": "13:00",
|
||||
"value": 13
|
||||
},
|
||||
{
|
||||
"label": "14:00",
|
||||
"value": 14
|
||||
},
|
||||
{
|
||||
"label": "15:00",
|
||||
"value": 15
|
||||
},
|
||||
{
|
||||
"label": "16:00",
|
||||
"value": 16
|
||||
},
|
||||
{
|
||||
"label": "17:00",
|
||||
"value": 17
|
||||
},
|
||||
{
|
||||
"label": "18:00",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"label": "19:00",
|
||||
"value": 19
|
||||
},
|
||||
{
|
||||
"label": "20:00",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"label": "21:00",
|
||||
"value": 21
|
||||
},
|
||||
{
|
||||
"label": "22:00",
|
||||
"value": 22
|
||||
},
|
||||
{
|
||||
"label": "23:00",
|
||||
"value": 23
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Every week",
|
||||
"key": "everyWeek",
|
||||
"description": "Triggers every week.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Day of the week",
|
||||
"key": "weekday",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"value": null,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "Monday",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": "Tuesday",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": "Wednesday",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"label": "Thursday",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"label": "Friday",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"label": "Saturday",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"label": "Sunday",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Time of day",
|
||||
"key": "hour",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"value": null,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "00:00",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"label": "01:00",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": "02:00",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": "03:00",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"label": "04:00",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"label": "05:00",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"label": "06:00",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"label": "07:00",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"label": "08:00",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"label": "09:00",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"label": "10:00",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"label": "11:00",
|
||||
"value": 11
|
||||
},
|
||||
{
|
||||
"label": "12:00",
|
||||
"value": 12
|
||||
},
|
||||
{
|
||||
"label": "13:00",
|
||||
"value": 13
|
||||
},
|
||||
{
|
||||
"label": "14:00",
|
||||
"value": 14
|
||||
},
|
||||
{
|
||||
"label": "15:00",
|
||||
"value": 15
|
||||
},
|
||||
{
|
||||
"label": "16:00",
|
||||
"value": 16
|
||||
},
|
||||
{
|
||||
"label": "17:00",
|
||||
"value": 17
|
||||
},
|
||||
{
|
||||
"label": "18:00",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"label": "19:00",
|
||||
"value": 19
|
||||
},
|
||||
{
|
||||
"label": "20:00",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"label": "21:00",
|
||||
"value": 21
|
||||
},
|
||||
{
|
||||
"label": "22:00",
|
||||
"value": 22
|
||||
},
|
||||
{
|
||||
"label": "23:00",
|
||||
"value": 23
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Every month",
|
||||
"key": "everyMonth",
|
||||
"description": "Triggers every month.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Day of the month",
|
||||
"key": "day",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"value": null,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": 1,
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": 2,
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": 3,
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"label": 4,
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"label": 5,
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"label": 6,
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"label": 7,
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"label": 8,
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"label": 9,
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"label": 10,
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"label": 11,
|
||||
"value": 11
|
||||
},
|
||||
{
|
||||
"label": 12,
|
||||
"value": 12
|
||||
},
|
||||
{
|
||||
"label": 13,
|
||||
"value": 13
|
||||
},
|
||||
{
|
||||
"label": 14,
|
||||
"value": 14
|
||||
},
|
||||
{
|
||||
"label": 15,
|
||||
"value": 15
|
||||
},
|
||||
{
|
||||
"label": 16,
|
||||
"value": 16
|
||||
},
|
||||
{
|
||||
"label": 17,
|
||||
"value": 17
|
||||
},
|
||||
{
|
||||
"label": 18,
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"label": 19,
|
||||
"value": 19
|
||||
},
|
||||
{
|
||||
"label": 20,
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"label": 21,
|
||||
"value": 21
|
||||
},
|
||||
{
|
||||
"label": 22,
|
||||
"value": 22
|
||||
},
|
||||
{
|
||||
"label": 23,
|
||||
"value": 23
|
||||
},
|
||||
{
|
||||
"label": 24,
|
||||
"value": 24
|
||||
},
|
||||
{
|
||||
"label": 25,
|
||||
"value": 25
|
||||
},
|
||||
{
|
||||
"label": 26,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"label": 27,
|
||||
"value": 27
|
||||
},
|
||||
{
|
||||
"label": 28,
|
||||
"value": 28
|
||||
},
|
||||
{
|
||||
"label": 29,
|
||||
"value": 29
|
||||
},
|
||||
{
|
||||
"label": 30,
|
||||
"value": 30
|
||||
},
|
||||
{
|
||||
"label": 31,
|
||||
"value": 31
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Time of day",
|
||||
"key": "hour",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"value": null,
|
||||
"variables": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "00:00",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"label": "01:00",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"label": "02:00",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"label": "03:00",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"label": "04:00",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"label": "05:00",
|
||||
"value": 5
|
||||
},
|
||||
{
|
||||
"label": "06:00",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"label": "07:00",
|
||||
"value": 7
|
||||
},
|
||||
{
|
||||
"label": "08:00",
|
||||
"value": 8
|
||||
},
|
||||
{
|
||||
"label": "09:00",
|
||||
"value": 9
|
||||
},
|
||||
{
|
||||
"label": "10:00",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"label": "11:00",
|
||||
"value": 11
|
||||
},
|
||||
{
|
||||
"label": "12:00",
|
||||
"value": 12
|
||||
},
|
||||
{
|
||||
"label": "13:00",
|
||||
"value": 13
|
||||
},
|
||||
{
|
||||
"label": "14:00",
|
||||
"value": 14
|
||||
},
|
||||
{
|
||||
"label": "15:00",
|
||||
"value": 15
|
||||
},
|
||||
{
|
||||
"label": "16:00",
|
||||
"value": 16
|
||||
},
|
||||
{
|
||||
"label": "17:00",
|
||||
"value": 17
|
||||
},
|
||||
{
|
||||
"label": "18:00",
|
||||
"value": 18
|
||||
},
|
||||
{
|
||||
"label": "19:00",
|
||||
"value": 19
|
||||
},
|
||||
{
|
||||
"label": "20:00",
|
||||
"value": 20
|
||||
},
|
||||
{
|
||||
"label": "21:00",
|
||||
"value": 21
|
||||
},
|
||||
{
|
||||
"label": "22:00",
|
||||
"value": 22
|
||||
},
|
||||
{
|
||||
"label": "23:00",
|
||||
"value": 23
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
};
|
||||
}
|
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 345 B |
10
packages/backend/src/apps/scheduler/common/cron-times.ts
Normal file
10
packages/backend/src/apps/scheduler/common/cron-times.ts
Normal 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;
|
@@ -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,
|
||||
};
|
||||
}
|
@@ -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;
|
||||
};
|
10
packages/backend/src/apps/scheduler/index.ts
Normal file
10
packages/backend/src/apps/scheduler/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
name: "Scheduler",
|
||||
key: "scheduler",
|
||||
iconUrl: "{BASE_URL}/apps/scheduler/assets/favicon.svg",
|
||||
docUrl: "https://automatisch.io/docs/scheduler",
|
||||
authDocUrl: "https://automatisch.io/docs/connections/scheduler",
|
||||
primaryColor: "0059F7",
|
||||
supportsConnections: false,
|
||||
requiresAuthentication: false,
|
||||
};
|
170
packages/backend/src/apps/scheduler/triggers/every-day/index.ts
Normal file
170
packages/backend/src/apps/scheduler/triggers/every-day/index.ts
Normal 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] };
|
||||
},
|
||||
};
|
@@ -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] };
|
||||
},
|
||||
};
|
@@ -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] };
|
||||
},
|
||||
};
|
187
packages/backend/src/apps/scheduler/triggers/every-week/index.ts
Normal file
187
packages/backend/src/apps/scheduler/triggers/every-week/index.ts
Normal 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] };
|
||||
},
|
||||
};
|
@@ -1,13 +0,0 @@
|
||||
import SendMessageToChannel from './actions/send-message-to-channel';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class Actions {
|
||||
sendMessageToChannel: SendMessageToChannel;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.sendMessageToChannel = new SendMessageToChannel(
|
||||
connectionData,
|
||||
parameters
|
||||
);
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -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;
|
||||
},
|
||||
};
|
@@ -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;
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -1,21 +0,0 @@
|
||||
import { WebClient } from '@slack/web-api';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class SendMessageToChannel {
|
||||
client: WebClient;
|
||||
parameters: IJSONObject;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.client = new WebClient(connectionData.accessToken as string);
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const result = await this.client.chat.postMessage({
|
||||
channel: this.parameters.channel as string,
|
||||
text: this.parameters.message as string,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-label="Slack" role="img"
|
||||
viewBox="0 0 512 512"><rect
|
||||
width="512" height="512"
|
||||
rx="15%"
|
||||
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>
|
||||
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>
|
Before Width: | Height: | Size: 533 B After Width: | Height: | Size: 531 B |
100
packages/backend/src/apps/slack/auth/index.ts
Normal file
100
packages/backend/src/apps/slack/auth/index.ts
Normal 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,
|
||||
};
|
12
packages/backend/src/apps/slack/auth/is-still-verified.ts
Normal file
12
packages/backend/src/apps/slack/auth/is-still-verified.ts
Normal 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;
|
34
packages/backend/src/apps/slack/auth/verify-credentials.ts
Normal file
34
packages/backend/src/apps/slack/auth/verify-credentials.ts
Normal 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;
|
@@ -1,39 +0,0 @@
|
||||
import type { IAuthentication, IApp, IJSONObject } from '@automatisch/types';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
|
||||
export default class Authentication implements IAuthentication {
|
||||
appData: IApp;
|
||||
connectionData: IJSONObject;
|
||||
client: WebClient;
|
||||
|
||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||
this.client = new WebClient();
|
||||
|
||||
this.connectionData = connectionData;
|
||||
this.appData = appData;
|
||||
}
|
||||
|
||||
async verifyCredentials() {
|
||||
const { bot_id: botId, user: screenName } = await this.client.auth.test({
|
||||
token: this.connectionData.accessToken as string,
|
||||
});
|
||||
|
||||
return {
|
||||
botId,
|
||||
screenName,
|
||||
token: this.connectionData.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
async isStillVerified() {
|
||||
try {
|
||||
await this.client.auth.test({
|
||||
token: this.connectionData.accessToken as string,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,10 +0,0 @@
|
||||
import ListChannels from './data/list-channels';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class Data {
|
||||
listChannels: ListChannels;
|
||||
|
||||
constructor(connectionData: IJSONObject) {
|
||||
this.listChannels = new ListChannels(connectionData);
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
import type { IJSONObject } from '@automatisch/types';
|
||||
import { WebClient } from '@slack/web-api';
|
||||
|
||||
export default class ListChannels {
|
||||
client: WebClient;
|
||||
|
||||
constructor(connectionData: IJSONObject) {
|
||||
this.client = new WebClient(connectionData.accessToken as string);
|
||||
}
|
||||
|
||||
async run() {
|
||||
const { channels } = await this.client.conversations.list();
|
||||
|
||||
return channels.map((channel) => {
|
||||
return {
|
||||
value: channel.id,
|
||||
name: channel.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
41
packages/backend/src/apps/slack/data/list-channels/index.ts
Normal file
41
packages/backend/src/apps/slack/data/list-channels/index.ts
Normal 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;
|
||||
},
|
||||
};
|
@@ -1,25 +1,8 @@
|
||||
import {
|
||||
IService,
|
||||
IAuthentication,
|
||||
IApp,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
import Authentication from './authentication';
|
||||
import Actions from './actions';
|
||||
import Data from './data';
|
||||
|
||||
export default class Slack implements IService {
|
||||
authenticationClient: IAuthentication;
|
||||
actions: Actions;
|
||||
data: Data;
|
||||
|
||||
constructor(
|
||||
appData: IApp,
|
||||
connectionData: IJSONObject,
|
||||
parameters: IJSONObject
|
||||
) {
|
||||
this.authenticationClient = new Authentication(appData, connectionData);
|
||||
this.data = new Data(connectionData);
|
||||
this.actions = new Actions(connectionData, parameters);
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'Slack',
|
||||
key: 'slack',
|
||||
iconUrl: '{BASE_URL}/apps/slack/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/connections/slack',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://slack.com/api',
|
||||
};
|
||||
|
@@ -1,149 +0,0 @@
|
||||
{
|
||||
"name": "Slack",
|
||||
"key": "slack",
|
||||
"iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/slack",
|
||||
"primaryColor": "2DAAE1",
|
||||
"fields": [
|
||||
{
|
||||
"key": "accessToken",
|
||||
"label": "Access Token",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"value": null,
|
||||
"placeholder": null,
|
||||
"description": "Access token of slack that Automatisch will connect to.",
|
||||
"docUrl": "https://automatisch.io/docs/slack#access-token",
|
||||
"clickToCopy": false
|
||||
}
|
||||
],
|
||||
"authenticationSteps": [
|
||||
{
|
||||
"step": 1,
|
||||
"type": "mutation",
|
||||
"name": "createConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"value": "{key}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "accessToken",
|
||||
"value": "{fields.accessToken}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"type": "mutation",
|
||||
"name": "verifyConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{createConnection.id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"reconnectionSteps": [
|
||||
{
|
||||
"step": 1,
|
||||
"type": "mutation",
|
||||
"name": "resetConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"type": "mutation",
|
||||
"name": "updateConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "accessToken",
|
||||
"value": "{fields.accessToken}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"type": "mutation",
|
||||
"name": "verifyConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "Send a message to channel",
|
||||
"key": "sendMessageToChannel",
|
||||
"description": "Send a message to a specific channel you specify.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
},
|
||||
{
|
||||
"key": "setupAction",
|
||||
"name": "Set up action",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Channel",
|
||||
"key": "channel",
|
||||
"type": "dropdown",
|
||||
"required": true,
|
||||
"description": "Pick a channel to send the message to.",
|
||||
"variables": false,
|
||||
"source": {
|
||||
"type": "query",
|
||||
"name": "getData",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"value": "listChannels"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Message text",
|
||||
"key": "message",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The content of your new message.",
|
||||
"variables": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test action"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/smtp/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/smtp",
|
||||
"primaryColor": "2DAAE1",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "host",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/twilio/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/twilio",
|
||||
"primaryColor": "f22f46",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "accountSid",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/twitch/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/twitch",
|
||||
"primaryColor": "2DAAE1",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import CreateTweet from './actions/create-tweet';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class Actions {
|
||||
createTweet: CreateTweet;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.createTweet = new CreateTweet(connectionData, parameters);
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class CreateTweet {
|
||||
client: TwitterApi;
|
||||
parameters: IJSONObject;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.client = new TwitterApi({
|
||||
appKey: connectionData.consumerKey,
|
||||
appSecret: connectionData.consumerSecret,
|
||||
accessToken: connectionData.accessToken,
|
||||
accessSecret: connectionData.accessSecret,
|
||||
} as TwitterApiTokens);
|
||||
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
async run() {
|
||||
const tweet = await this.client.v1.tweet(this.parameters.tweet as string);
|
||||
return tweet;
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Twitter" role="img" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="15%" fill="#1da1f2"/>
|
||||
<path fill="#fff" d="M437 152a72 72 0 01-40 12a72 72 0 0032-40a72 72 0 01-45 17a72 72 0 00-122 65a200 200 0 01-145-74a72 72 0 0022 94a72 72 0 01-32-7a72 72 0 0056 69a72 72 0 01-32 1a72 72 0 0067 50a200 200 0 01-105 29a200 200 0 00309-179a200 200 0 0035-37"/>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 421 B |
35
packages/backend/src/apps/twitter/auth/create-auth-data.ts
Normal file
35
packages/backend/src/apps/twitter/auth/create-auth-data.ts
Normal 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}`
|
||||
);
|
||||
}
|
||||
}
|
219
packages/backend/src/apps/twitter/auth/index.ts
Normal file
219
packages/backend/src/apps/twitter/auth/index.ts
Normal 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,
|
||||
};
|
13
packages/backend/src/apps/twitter/auth/is-still-verified.ts
Normal file
13
packages/backend/src/apps/twitter/auth/is-still-verified.ts
Normal 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;
|
24
packages/backend/src/apps/twitter/auth/verify-credentials.ts
Normal file
24
packages/backend/src/apps/twitter/auth/verify-credentials.ts
Normal 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;
|
@@ -1,66 +0,0 @@
|
||||
import type {
|
||||
IAuthentication,
|
||||
IApp,
|
||||
IField,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
||||
|
||||
export default class Authentication implements IAuthentication {
|
||||
appData: IApp;
|
||||
connectionData: IJSONObject;
|
||||
client: TwitterApi;
|
||||
|
||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||
this.appData = appData;
|
||||
this.connectionData = connectionData;
|
||||
|
||||
const clientParams = {
|
||||
appKey: connectionData.consumerKey,
|
||||
appSecret: connectionData.consumerSecret,
|
||||
accessToken: connectionData.accessToken,
|
||||
accessSecret: connectionData.accessSecret,
|
||||
} as TwitterApiTokens;
|
||||
|
||||
this.client = new TwitterApi(clientParams);
|
||||
}
|
||||
|
||||
async createAuthData() {
|
||||
const appFields = this.appData.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
const callbackUrl = appFields.value;
|
||||
|
||||
const authLink = await this.client.generateAuthLink(callbackUrl);
|
||||
|
||||
return {
|
||||
url: authLink.url,
|
||||
accessToken: authLink.oauth_token,
|
||||
accessSecret: authLink.oauth_token_secret,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyCredentials() {
|
||||
const verifiedCredentials = await this.client.login(
|
||||
this.connectionData.oauthVerifier as string
|
||||
);
|
||||
|
||||
return {
|
||||
consumerKey: this.connectionData.consumerKey,
|
||||
consumerSecret: this.connectionData.consumerSecret,
|
||||
accessToken: verifiedCredentials.accessToken,
|
||||
accessSecret: verifiedCredentials.accessSecret,
|
||||
userId: verifiedCredentials.userId,
|
||||
screenName: verifiedCredentials.screenName,
|
||||
};
|
||||
}
|
||||
|
||||
async isStillVerified() {
|
||||
try {
|
||||
await this.client.currentUser();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
44
packages/backend/src/apps/twitter/common/generate-request.ts
Normal file
44
packages/backend/src/apps/twitter/common/generate-request.ts
Normal 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;
|
14
packages/backend/src/apps/twitter/common/get-current-user.ts
Normal file
14
packages/backend/src/apps/twitter/common/get-current-user.ts
Normal 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;
|
@@ -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;
|
@@ -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;
|
79
packages/backend/src/apps/twitter/common/get-user-tweets.ts
Normal file
79
packages/backend/src/apps/twitter/common/get-user-tweets.ts
Normal 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;
|
23
packages/backend/src/apps/twitter/common/oauth-client.ts
Normal file
23
packages/backend/src/apps/twitter/common/oauth-client.ts
Normal 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;
|
@@ -1,25 +1,8 @@
|
||||
import {
|
||||
IService,
|
||||
IAuthentication,
|
||||
IApp,
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
import Authentication from './authentication';
|
||||
import Triggers from './triggers';
|
||||
import Actions from './actions';
|
||||
|
||||
export default class Twitter implements IService {
|
||||
authenticationClient: IAuthentication;
|
||||
triggers: Triggers;
|
||||
actions: Actions;
|
||||
|
||||
constructor(
|
||||
appData: IApp,
|
||||
connectionData: IJSONObject,
|
||||
parameters: IJSONObject
|
||||
) {
|
||||
this.authenticationClient = new Authentication(appData, connectionData);
|
||||
this.triggers = new Triggers(connectionData, parameters);
|
||||
this.actions = new Actions(connectionData, parameters);
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'Twitter',
|
||||
key: 'twitter',
|
||||
iconUrl: '{BASE_URL}/apps/twitter/assets/favicon.svg',
|
||||
authDocUrl: 'https://automatisch.io/docs/connections/twitter',
|
||||
supportsConnections: true,
|
||||
baseUrl: 'https://api.twitter.com',
|
||||
};
|
||||
|
@@ -1,320 +0,0 @@
|
||||
{
|
||||
"name": "Twitter",
|
||||
"key": "twitter",
|
||||
"iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/twitter",
|
||||
"primaryColor": "2DAAE1",
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
"label": "OAuth Redirect URL",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": true,
|
||||
"value": "{WEB_APP_URL}/app/twitter/connections/add",
|
||||
"placeholder": null,
|
||||
"description": "When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.",
|
||||
"docUrl": "https://automatisch.io/docs/twitter#oauth-redirect-url",
|
||||
"clickToCopy": true
|
||||
},
|
||||
{
|
||||
"key": "consumerKey",
|
||||
"label": "Consumer Key",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"value": null,
|
||||
"placeholder": null,
|
||||
"description": null,
|
||||
"docUrl": "https://automatisch.io/docs/twitter#consumer-key",
|
||||
"clickToCopy": false
|
||||
},
|
||||
{
|
||||
"key": "consumerSecret",
|
||||
"label": "Consumer Secret",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"value": null,
|
||||
"placeholder": null,
|
||||
"description": null,
|
||||
"docUrl": "https://automatisch.io/docs/twitter#consumer-secret",
|
||||
"clickToCopy": false
|
||||
}
|
||||
],
|
||||
"authenticationSteps": [
|
||||
{
|
||||
"step": 1,
|
||||
"type": "mutation",
|
||||
"name": "createConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "key",
|
||||
"value": "{key}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "consumerKey",
|
||||
"value": "{fields.consumerKey}"
|
||||
},
|
||||
{
|
||||
"name": "consumerSecret",
|
||||
"value": "{fields.consumerSecret}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"type": "mutation",
|
||||
"name": "createAuthData",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{createConnection.id}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"type": "openWithPopup",
|
||||
"name": "openAuthPopup",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "url",
|
||||
"value": "{createAuthData.url}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"type": "mutation",
|
||||
"name": "updateConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{createConnection.id}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "oauthVerifier",
|
||||
"value": "{openAuthPopup.oauth_verifier}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"type": "mutation",
|
||||
"name": "verifyConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{createConnection.id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"reconnectionSteps": [
|
||||
{
|
||||
"step": 1,
|
||||
"type": "mutation",
|
||||
"name": "resetConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"type": "mutation",
|
||||
"name": "updateConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "consumerKey",
|
||||
"value": "{fields.consumerKey}"
|
||||
},
|
||||
{
|
||||
"name": "consumerSecret",
|
||||
"value": "{fields.consumerSecret}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"type": "mutation",
|
||||
"name": "createAuthData",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"type": "openWithPopup",
|
||||
"name": "openAuthPopup",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "url",
|
||||
"value": "{createAuthData.url}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 5,
|
||||
"type": "mutation",
|
||||
"name": "updateConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
},
|
||||
{
|
||||
"name": "formattedData",
|
||||
"value": null,
|
||||
"properties": [
|
||||
{
|
||||
"name": "oauthVerifier",
|
||||
"value": "{openAuthPopup.oauth_verifier}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": 6,
|
||||
"type": "mutation",
|
||||
"name": "verifyConnection",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "id",
|
||||
"value": "{connection.id}"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"triggers": [
|
||||
{
|
||||
"name": "My Tweet",
|
||||
"key": "myTweet",
|
||||
"description": "Will be triggered when you tweet something new.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "User Tweet",
|
||||
"key": "userTweet",
|
||||
"description": "Will be triggered when a specific user tweet something new.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Username",
|
||||
"key": "username",
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Search Tweet",
|
||||
"key": "searchTweet",
|
||||
"description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
},
|
||||
{
|
||||
"key": "chooseTrigger",
|
||||
"name": "Set up a trigger",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Search Term",
|
||||
"key": "searchTerm",
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test trigger"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "Create Tweet",
|
||||
"key": "createTweet",
|
||||
"description": "Will create a tweet.",
|
||||
"substeps": [
|
||||
{
|
||||
"key": "chooseAccount",
|
||||
"name": "Choose account"
|
||||
},
|
||||
{
|
||||
"key": "chooseAction",
|
||||
"name": "Set up action",
|
||||
"arguments": [
|
||||
{
|
||||
"label": "Tweet body",
|
||||
"key": "tweet",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The content of your new tweet.",
|
||||
"variables": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "testStep",
|
||||
"name": "Test action"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import MyTweet from './triggers/my-tweet';
|
||||
import SearchTweet from './triggers/search-tweet';
|
||||
|
||||
export default class Triggers {
|
||||
myTweet: MyTweet;
|
||||
searchTweet: SearchTweet;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.myTweet = new MyTweet(connectionData);
|
||||
this.searchTweet = new SearchTweet(connectionData, parameters);
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class MyTweet {
|
||||
client: TwitterApi;
|
||||
|
||||
constructor(connectionData: IJSONObject) {
|
||||
this.client = new TwitterApi({
|
||||
appKey: connectionData.consumerKey,
|
||||
appSecret: connectionData.consumerSecret,
|
||||
accessToken: connectionData.accessToken,
|
||||
accessSecret: connectionData.accessSecret,
|
||||
} as TwitterApiTokens);
|
||||
}
|
||||
|
||||
async run() {
|
||||
const response = await this.client.currentUser();
|
||||
const username = response.screen_name;
|
||||
|
||||
const userTimeline = await this.client.v1.userTimelineByUsername(username);
|
||||
const fetchedTweets = userTimeline.tweets;
|
||||
|
||||
return fetchedTweets;
|
||||
}
|
||||
}
|
@@ -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 });
|
||||
},
|
||||
};
|
@@ -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($);
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -1,58 +0,0 @@
|
||||
import TwitterApi, { TwitterApiTokens } from 'twitter-api-v2';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
|
||||
export default class SearchTweet {
|
||||
client: TwitterApi;
|
||||
parameters: IJSONObject;
|
||||
|
||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
||||
this.client = new TwitterApi({
|
||||
appKey: connectionData.consumerKey,
|
||||
appSecret: connectionData.consumerSecret,
|
||||
accessToken: connectionData.accessToken,
|
||||
accessSecret: connectionData.accessSecret,
|
||||
} as TwitterApiTokens);
|
||||
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
async run(startTime: Date) {
|
||||
const tweets = [];
|
||||
|
||||
const response = await this.client.v2.search(
|
||||
this.parameters.searchTerm as string,
|
||||
{
|
||||
max_results: 50,
|
||||
'tweet.fields': 'created_at',
|
||||
}
|
||||
);
|
||||
|
||||
for await (const tweet of response.data.data) {
|
||||
if (new Date(tweet.created_at).getTime() <= startTime.getTime()) {
|
||||
break;
|
||||
}
|
||||
|
||||
tweets.push(tweet);
|
||||
|
||||
if (response.data.meta.next_token) {
|
||||
await response.fetchNext();
|
||||
}
|
||||
}
|
||||
|
||||
return tweets;
|
||||
}
|
||||
|
||||
async testRun() {
|
||||
const response = await this.client.v2.search(
|
||||
this.parameters.searchTerm as string,
|
||||
{
|
||||
max_results: 10,
|
||||
'tweet.fields': 'created_at',
|
||||
}
|
||||
);
|
||||
|
||||
const mostRecentTweet = response.data.data[0];
|
||||
|
||||
return [mostRecentTweet];
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
@@ -5,14 +5,12 @@ import type {
|
||||
IJSONObject,
|
||||
} from '@automatisch/types';
|
||||
import { URLSearchParams } from 'url';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
|
||||
|
||||
export default class Authentication implements IAuthentication {
|
||||
appData: IApp;
|
||||
connectionData: IJSONObject;
|
||||
client: AxiosInstance = axios.create({
|
||||
baseURL: 'https://api.typeform.com',
|
||||
});
|
||||
client: IHttpClient;
|
||||
|
||||
scope: string[] = [
|
||||
'forms:read',
|
||||
@@ -27,6 +25,7 @@ export default class Authentication implements IAuthentication {
|
||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
||||
this.connectionData = connectionData;
|
||||
this.appData = appData;
|
||||
this.client = createHttpClient({ baseURL: 'https://api.typeform.com' });
|
||||
}
|
||||
|
||||
get oauthRedirectUrl() {
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"iconUrl": "{BASE_URL}/apps/typeform/assets/favicon.svg",
|
||||
"docUrl": "https://automatisch.io/docs/typeform",
|
||||
"primaryColor": "5865f2",
|
||||
"supportsConnections": true,
|
||||
"fields": [
|
||||
{
|
||||
"key": "oAuthRedirectUrl",
|
||||
|
@@ -13,6 +13,7 @@ type AppConfig = {
|
||||
postgresHost: string;
|
||||
postgresUsername: string;
|
||||
postgresPassword?: string;
|
||||
version: string;
|
||||
postgresEnableSsl: boolean;
|
||||
baseUrl: string;
|
||||
encryptionKey: string;
|
||||
@@ -20,12 +21,15 @@ type AppConfig = {
|
||||
serveWebAppSeparately: boolean;
|
||||
redisHost: string;
|
||||
redisPort: number;
|
||||
enableBullMQDashboard: boolean;
|
||||
telemetryEnabled: boolean;
|
||||
};
|
||||
|
||||
const host = process.env.HOST || 'localhost';
|
||||
const protocol = process.env.PROTOCOL || 'http';
|
||||
const port = process.env.PORT || '3000';
|
||||
const serveWebAppSeparately = process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
||||
const serveWebAppSeparately =
|
||||
process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false;
|
||||
|
||||
let webAppUrl = `${protocol}://${host}:${port}`;
|
||||
if (serveWebAppSeparately) {
|
||||
@@ -42,8 +46,9 @@ const appConfig: AppConfig = {
|
||||
port,
|
||||
appEnv: appEnv,
|
||||
isDev: appEnv === 'development',
|
||||
version: process.env.npm_package_version,
|
||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||
postgresPort: parseInt(process.env.POSTGRES_PORT|| '5432'),
|
||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
postgresHost: process.env.POSTGRES_HOST || 'localhost',
|
||||
postgresUsername:
|
||||
process.env.POSTGRES_USERNAME || 'automatisch_development_user',
|
||||
@@ -54,8 +59,11 @@ const appConfig: AppConfig = {
|
||||
serveWebAppSeparately,
|
||||
redisHost: process.env.REDIS_HOST || '127.0.0.1',
|
||||
redisPort: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
enableBullMQDashboard:
|
||||
process.env.ENABLE_BULLMQ_DASHBOARD === 'true' ? true : false,
|
||||
baseUrl,
|
||||
webAppUrl,
|
||||
telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true,
|
||||
};
|
||||
|
||||
if (!appConfig.encryptionKey) {
|
||||
|
@@ -1,4 +1,8 @@
|
||||
import process from 'process';
|
||||
// The following two lines are required to get count values as number.
|
||||
// More info: https://github.com/knex/knex/issues/387#issuecomment-51554522
|
||||
import pg from 'pg';
|
||||
pg.types.setTypeParser(20, 'text', parseInt);
|
||||
import knex from 'knex';
|
||||
import type { Knex } from 'knex';
|
||||
import knexConfig from '../../knexfile';
|
||||
@@ -8,10 +12,12 @@ export const client: Knex = knex(knexConfig);
|
||||
|
||||
const CONNECTION_REFUSED = 'ECONNREFUSED';
|
||||
|
||||
client.raw('SELECT 1')
|
||||
.catch((err) => {
|
||||
if (err.code === CONNECTION_REFUSED) {
|
||||
logger.error('Make sure you have installed PostgreSQL and it is running.', err);
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
client.raw('SELECT 1').catch((err) => {
|
||||
if (err.code === CONNECTION_REFUSED) {
|
||||
logger.error(
|
||||
'Make sure you have installed PostgreSQL and it is running.',
|
||||
err
|
||||
);
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('connections', (table) => {
|
||||
table.boolean('draft').defaultTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('connections', (table) => {
|
||||
table.dropColumn('draft');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('flows', (table) => {
|
||||
table.timestamp('published_at').nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('flows', (table) => {
|
||||
table.dropColumn('published_at');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('executions', (table) => {
|
||||
table.string('internal_id');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('executions', (table) => {
|
||||
table.dropColumn('internal_id');
|
||||
});
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('execution_steps', (table) => {
|
||||
table.jsonb('error_details');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
return knex.schema.table('execution_steps', (table) => {
|
||||
table.dropColumn('error_details');
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user