Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 8d150b0

Browse files
authoredOct 29, 2023
feat: simplify configuration (#105)
1 parent bce81af commit 8d150b0

28 files changed

+534
-377
lines changed
 

‎.env.example

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Data source
1+
# Data source
22
TWITTER_HANDLE=username
33

4-
// API
4+
# API
55
TWITTER_USERNAME=username
66
TWITTER_PASSWORD=password
77
MASTODON_INSTANCE=mastodon.social
@@ -10,12 +10,10 @@ BLUESKY_INSTANCE=
1010
BLUESKY_IDENTIFIER=
1111
BLUESKY_PASSWORD=
1212

13-
// Touitomamout
14-
INSTANCE_ID=
15-
EXECUTION=manual
16-
# EXECUTION=pm2
13+
# Touitomamout
1714
SYNC_MASTODON=true
1815
SYNC_BLUESKY=true
16+
SYNC_FREQUENCY_MIN=30
1917
SYNC_PROFILE_DESCRIPTION=false
2018
SYNC_PROFILE_PICTURE=false
2119
SYNC_PROFILE_HEADER=false

‎README.md

+5-88
Original file line numberDiff line numberDiff line change
@@ -27,92 +27,9 @@ Please find the project documentation here:
2727

2828
[<img src="https://github.com/louisgrasset/touitomamout/raw/main/.github/docs/documentation-center.svg" width="300px"/>](https://louisgrasset.github.io/touitomamout/docs/discover)
2929

30+
## Dependencies
31+
Kudos to the following projects that made Touitomamout project possible 🙏
32+
- 🦤 [twitter-scraper](https://github.com/the-convocation/twitter-scraper)
33+
- 🦣 [masto.js](https://github.com/neet/masto.js)
34+
- ☁️[atproto](https://github.com/bluesky-social/atproto)
3035

31-
## Installation
32-
**Clone** the project
33-
```bash
34-
git clone git@github.com:louisgrasset/touitomamout.git
35-
```
36-
37-
**Install** dependencies & build the project
38-
```bash
39-
npm ci && npm run build
40-
```
41-
## Configuration
42-
Touitomamout relies on two APIs:
43-
- [Mastodon](https://docs.joinmastodon.org/client/intro/)
44-
- [Twitter-Scraper](https://github.com/the-convocation/twitter-scraper)
45-
46-
### Mastodon configuration
47-
In order to communicate with the mastodon instance, you'll have to generate an API Token. It is totally free. Reminder: your application name will be publicly visible.
48-
1. Go to your account's application page: `https://{yourinstance.tld}/settings/applications/new`
49-
2. Create a new application with the following scopes:
50-
- `read:accounts`: get your mastodon account username
51-
- `write:media`: post medias
52-
- `write:statuses`: post toots
53-
- `write:accounts`: update your profile
54-
3. Populate the [Environment](#Environment) section with your `access token`.
55-
56-
### Twitter configuration
57-
The tweets retrieval by itself can be done without Twitter credentials. But keep in mind that twitter currently blocks guests to access users' replies.
58-
Touitomamout is trying to restore the previous session, so you'll not get spammed by the Twitter security team for each connection.
59-
60-
61-
> **Note**
62-
>
63-
> The configuration allows you to sync a first account and authenticate with a secondary account for two reasons:
64-
> 1. Currently, there is no simple way to authenticate with an account having 2FA enabled, so you may not want to lower your main account security.
65-
> 2. Because this project is running with a non-official API, you may not want to put your account at risk.
66-
67-
68-
### Environment
69-
First things first, please copy the [`.env.example`](https://github.com/louisgrasset/touitomamout/blob/main/.env.example) file to `.env`.
70-
Then, please fill each variable.
71-
72-
> **Warning**
73-
>
74-
> Do not forget to properly choose the `EXECUTION` variable.
75-
> Two values are allowed:
76-
> 1. `manual`: a simple node script execution
77-
> 2. `pm2`: spawns as a new PM2 process, named with `touitomamout-${instance_id}` pattern.
78-
79-
80-
### Multiple instances
81-
This project supports a multiple instances mode. To do so, simply provide multiple `.env` files such as `.env.instance1` and `.env.instance2`.
82-
83-
`deploy` & `deploy:update` scripts will handle them properly.
84-
85-
## Run it
86-
87-
### Manually
88-
You can simply run a `node ./dist/index.js .env` and have a one shot sync of your recent tweets.
89-
90-
> **Note**
91-
>
92-
> Don't forget to replace `.env` with the right .env filename.
93-
94-
95-
### Cron
96-
Because automation is cool, feel free to run that script everytime you need to.
97-
Simply create your cron [here](https://crontab.guru).
98-
99-
### PM2 support
100-
[PM2](https://pm2.keymetrics.io/) is a utility that allows you to monitor and run periodically your node scripts.
101-
Thus, it can be useful for some users to deploy Touitomamout to a PM2 instance.
102-
103-
#### **PM2**: First run
104-
```bash
105-
npm run deploy
106-
```
107-
108-
#### **PM2**: Update your instance after a code update
109-
Your instance will be removed and will be generated again with the latest codebase.
110-
Your `cache.instance.json` file is kept, so you won't have duplicated toots.
111-
```bash
112-
npm ci &&
113-
npm run build &&
114-
npm run deploy:update
115-
```
116-
117-
### Docker
118-
You can alternatively rely on docker and use the `docker-compose.yml` file.

‎deployment/deploy.sh

-27
This file was deleted.

‎deployment/pm2.sh

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/sh
2+
3+
script_path=$(realpath "$0")
4+
script_dir=$(dirname "$script_path")
5+
6+
touitomamout="$script_dir/../dist/index.js"
7+
8+
# For each env file found (except .env.example)
9+
for env in .env*; do
10+
if [[ $env != *.example ]]; then
11+
# Prevent startup with PM2 if the internal DAEMON is set to true. PM2 will handle the cron itself.
12+
daemon=$(grep "DAEMON=" "$env" | cut -d '=' -f 2)
13+
14+
if [[ daemon == "true" ]]; then
15+
echo "DAEMON is enabled. Disable it before deploying Touitomamout with PM2. Please check your .env file."
16+
exit 1
17+
fi
18+
19+
# Handle the instance name
20+
name=$(grep "TWITTER_HANDLE=" "$env" | cut -d '=' -f 2)
21+
22+
if [[ -z $name ]]; then
23+
echo "No value found for TWITTER_HANDLE in $env. Please check your .env file."
24+
exit 1
25+
fi
26+
27+
# Handle the sync frequency
28+
frequency=$(grep "SYNC_FREQUENCY_MIN=" "$env" | cut -d '=' -f 2)
29+
30+
if [[ -z $frequency ]]; then
31+
echo "No value found for SYNC_FREQUENCY_MIN in $env. Using default value. Please check your .env file."
32+
frequency=30
33+
fi
34+
35+
# If called with --update flag, delete the instance before starting it again
36+
if [[ $1 == "--update" ]]; then
37+
pm2 del touitomamout-"$name"
38+
fi
39+
40+
pm2 start "$touitomamout" --name touitomamout-"$name" --no-autorestart --cron "*/$frequency * * * *" -- "$env"
41+
pm2 save
42+
fi
43+
done

‎docker-compose.source.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version: '3.9'
2+
3+
services:
4+
touitomamout:
5+
container_name: "touitomamout"
6+
build:
7+
context: ./ # ← This will build the image from the source code
8+
restart: unless-stopped
9+
environment:
10+
- ENV_FILE=/data/.env
11+
- STORAGE_DIR=/data
12+
volumes:
13+
- ./data:/data

‎docker-compose.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ version: '3.9'
22

33
services:
44
touitomamout:
5-
build:
6-
context: ./
5+
container_name: "touitomamout"
6+
image: louisgrasset/touitomamout:latest # Or "ghcr.io/louisgrasset/touitomamout:latest"
7+
restart: unless-stopped
78
environment:
89
- ENV_FILE=/data/.env
910
- STORAGE_DIR=/data
10-
- DAEMON_PERIOD_MIN=1
1111
volumes:
1212
- ./data:/data

‎docs/docs/configuration/cron.md

-16
This file was deleted.

‎docs/docs/configuration/cron.mdx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
sidebar_position: 3
3+
title: Configure with Cron
4+
tags: [
5+
'configuration',
6+
'cron',
7+
]
8+
---
9+
import SectionPullInstallProject from "./sections/_section_pull_install_project.md"
10+
import TipFileGenerator from "./sections/_tip-file-generator.md"
11+
12+
# ⏰ Cron configuration
13+
14+
## Installation
15+
<SectionPullInstallProject/>
16+
17+
## Define environment variables
18+
3 types of environment variables are required to run the sync:
19+
- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables).
20+
- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-).
21+
- **Cron variables**: required for the cron, get them [here](/docs/resources/environment-variables#configuration-with-cron-).
22+
23+
<TipFileGenerator />
24+
25+
Once your env variables are ready, inject them in a .env.{handle_to_sync} file. (e.g: `.env.signalapp`)
26+
27+
## Run it!
28+
29+
> You can create your own cron expression here: [crontab.guru](https://crontab.guru/).
30+
31+
First, enter the cron editor:
32+
```bash
33+
crontab -e
34+
```
35+
36+
Then, add the following line to run the sync every 30 minutes:
37+
```bash
38+
*/30 * * * * cd /home/{user}/services/touitmamout && node ./dist/index.js .env
39+
```
40+
41+
Finally, save and exit the editor.

‎docs/docs/configuration/docker.md

-16
This file was deleted.

‎docs/docs/configuration/docker.mdx

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
sidebar_position: 2
3+
title: Configure with Docker
4+
tags: [
5+
'configuration',
6+
'docker',
7+
]
8+
---
9+
import TipFileGenerator from "./sections/_tip-file-generator.md"
10+
11+
# 🐳 Docker configuration
12+
13+
## Define environment variables
14+
3 types of environment variables are required to run the sync:
15+
- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables)
16+
- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-)
17+
- **Docker variables**: required for the docker container, get them [here](/docs/resources/environment-variables#configuration-with-docker-)
18+
19+
<TipFileGenerator />
20+
21+
## Create the touitomamout docker container
22+
Two ways of creating it: either by using the image or by building it from the source code. The first one is a bit easier, but features will be the same in both cases.
23+
24+
---
25+
📦 Releases are published on the following registries:
26+
- [Docker Hub](https://hub.docker.com/r/louisgrasset/touitomamout) (`louisgrasset/touitomamout`)
27+
- [Github (GHCR)](https://github.com/louisgrasset/touitomamout/pkgs/container/touitomamout) (`ghcr.io/louisgrasset/touitomamout`)
28+
29+
| Image tag | Description |
30+
|-----------|----------------------------------------------------|
31+
| `latest` | Corresponds to the latest release. |
32+
| `dev` | Reflects the most recent changes on "main" branch. |
33+
| `x.x.x` | A specific version. |
34+
35+
## From the image
36+
```yml title="docker-compose.yml"
37+
version: '3.9'
38+
39+
services:
40+
touitomamout:
41+
container_name: "touitomamout"
42+
image: louisgrasset/touitomamout:latest # Or "ghcr.io/louisgrasset/touitomamout:latest"
43+
restart: unless-stopped
44+
environment:
45+
- ENV_FILE=/data/.env
46+
- STORAGE_DIR=/data
47+
volumes:
48+
- ./data:/data
49+
```
50+
## From the source code
51+
```yml title="docker-compose.source.yml"
52+
version: '3.9'
53+
54+
services:
55+
touitomamout:
56+
container_name: "touitomamout"
57+
build:
58+
context: ./ # ← This will build the image from the source code
59+
restart: unless-stopped
60+
environment:
61+
- ENV_FILE=/data/.env
62+
- STORAGE_DIR=/data
63+
volumes:
64+
- ./data:/data
65+
```
66+
67+
## Run it!
68+
Simply run this command to start the container.
69+
```bash
70+
docker-compose up -d
71+
```

‎docs/docs/configuration/manual-sync.md ‎docs/docs/configuration/manual-sync.mdx

+12
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,23 @@ tags: [
66
'manual sync',
77
]
88
---
9+
import SectionPullInstallProject from "./sections/_section_pull_install_project.md"
10+
import TipFileGenerator from "./sections/_tip-file-generator.md"
911

1012
# 👏️️ Manual sync configuration
1113

14+
## Installation
15+
<SectionPullInstallProject/>
16+
1217
## Define environment variables
1318
3 types of environment variables are required to run the sync:
1419
- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables).
1520
- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-).
1621
- **Manual sync variables**: required for the manual sync, get them [here](/docs/resources/environment-variables#configuration-with-manual-sync-).
22+
23+
<TipFileGenerator />
24+
25+
## Run it!
26+
```bash
27+
node ./dist/index.js .env.{handle_to_sync}
28+
```

‎docs/docs/configuration/pm2.md

-16
This file was deleted.

‎docs/docs/configuration/pm2.mdx

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
sidebar_position: 4
3+
title: Configure with PM2
4+
tags: [
5+
'configuration',
6+
'pm2',
7+
]
8+
---
9+
import SectionPullInstallProject from "./sections/_section_pull_install_project.md"
10+
import TipFileGenerator from "./sections/_tip-file-generator.md"
11+
12+
# ⏲️ PM2 configuration
13+
14+
## Installation
15+
<SectionPullInstallProject/>
16+
17+
## Define environment variables
18+
3 types of environment variables are required to run the sync:
19+
- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables).
20+
- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-).
21+
- **PM2 variables**: required for the PM2 instance, get them [here](/docs/resources/environment-variables#configuration-with-pm2-).
22+
23+
<TipFileGenerator />
24+
25+
Once your env variables are ready, inject them in a .env.{handle_to_sync} file. (e.g: `.env.signalapp`)
26+
27+
## Run it!
28+
29+
Touitomamout comes with a script that will automatically handle the PM2 instances management for you.
30+
31+
Simply run the following command, and all your `.env.{handle_to_sync}` files will be used by PM2 to create a dedicated Touitomamout instance for each.
32+
33+
```bash
34+
npm run pm2
35+
```
36+
37+
:::info I upgraded the project to the latest version, what should I do?
38+
If you upgrade the project, you will need to update the PM2 instance(s) to use the latest version of the project. But that's as quick as running the following command:
39+
```bash
40+
npm run pm2:update
41+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Assuming you have a folder /home/{user}/services
2+
3+
**Clone** the project
4+
```bash
5+
cd /home/{user}/services && git clone https://github.com/louisgrasset/touitomamout.git
6+
```
7+
8+
**Install** dependencies & build the project
9+
```bash
10+
npm ci && npm run build
11+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:::tip Environment file generator
2+
3+
Feel free to generate your `.env` file with the [generator](/docs/tools/env-generator)
4+
:::

‎docs/docs/resources/_category_.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"label": "Resources",
33
"position": 3,
4-
"collapsed": true,
4+
"collapsed": false,
55
"link": {
66
"type": "generated-index"
77
}

‎docs/docs/resources/credentials.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
title: Credentials
3+
---
4+
5+
# Credentials
6+
7+
To authenticate with the different platforms, you have to provide credentials. You'll find here the list of all the credentials by platform.
8+
9+
## Mastodon 🦣
10+
To communicate with the mastodon instance, you'll have to generate an API Token. It is totally free.
11+
12+
:::info Application name
13+
Your application name will be publicly **visible**. We recommend you to use the name of the tool you are using when you are creating the application.
14+
In this case : `Touitomamout`.
15+
:::
16+
17+
1. Go to your account's application page: `https://{yourinstance.tld}/settings/applications/new`
18+
2. Create a new application with the following scopes:
19+
- `read:accounts`: get your mastodon account username
20+
- `write:media`: post medias
21+
- `write:statuses`: post toots
22+
- `write:accounts`: update your profile
23+
3. Copy the token and write it in the .env file as `MASTODON_ACCESS_TOKEN`.
24+
25+
## Bluesky ☁️

‎docs/docs/resources/environment-variables.md

+43-29
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ environment variables by usage. `(📌 means the variable is required for the co
99

1010
## Core variables
1111

12-
| Variable | Default | Category | Description |
13-
|---------------------|:-------:|-------------------|------------------------------------------------|
14-
| `TWITTER_HANDLE` 📌 || **Twitter**::data | The twitter username you want to sync. |
15-
| `TWITTER_USERNAME` || **Twitter**::auth | The twitter username used to log into twitter. |
16-
| `TWITTER_PASSWORD` || **Twitter**::auth | The twitter password used to log into twitter. |
12+
| Variable | Default | Category | Description |
13+
|---------------------|:------------------:|-------------------|--------------------------------------------------------------------------------------------|
14+
| `TWITTER_HANDLE` 📌 || **Twitter**::data | The twitter username you want to sync. |
15+
| `TWITTER_USERNAME` || **Twitter**::auth | The twitter username used to log into twitter. |
16+
| `TWITTER_PASSWORD` || **Twitter**::auth | The twitter password used to log into twitter. |
17+
1718

1819
:::caution Twitter auth
1920

@@ -24,45 +25,58 @@ Even if `TWITTER_USERNAME` & `TWITTER_PASSWORD` are optional, these variables ar
2425

2526
## To sync to Mastodon 🦣
2627

27-
| Variable | Default | Category | Description |
28-
|----------------------------|:-------:|--------------------|---------------------------------------------------------------|
29-
| 📌 `MASTODON_INSTANCE` || **Mastodon**::auth | The mastodon instance the account is registered on. |
30-
| 📌 `MASTODON_ACCESS_TOKEN` || **Mastodon**::auth | The mastodon access token to authenticated the sync requests. |
28+
| Variable | Default | Category | Description |
29+
|----------------------------|:-------:|--------------------------------------------------------------------------|---------------------------------------------------------------|
30+
| 📌 `MASTODON_INSTANCE` || ![**Mastodon**::auth](https://img.shields.io/badge/Mastodon-Auth-6364FF) | The mastodon instance the account is registered on. |
31+
| 📌 `MASTODON_ACCESS_TOKEN` || ![**Mastodon**::auth](https://img.shields.io/badge/Mastodon-Auth-6364FF) | The mastodon access token to authenticated the sync requests. |
3132

3233
## To sync to Bluesky ☁️
3334

34-
| Variable | Default | Category | Description |
35-
|-------------------------|:-------:|-------------------|--------------------------------------------------------------------------------------------------------------------|
36-
| 📌 `BLUESKY_INSTANCE` || **Bluesky**::auth | The bluesky instance the account is registered on.<br/>_(eg: `bsky.social`)_ |
37-
| 📌 `BLUESKY_IDENTIFIER` || **Bluesky**::auth | The bluesky user identifier.<br/>_(eg: `handle.bsky.social`)_ |
38-
| 📌 `BLUESKY_PASSWORD` || **Bluesky**::auth | The bluesky password.<br/>_(Can be a user password or an [app password](https://bsky.app/settings/app-passwords))_ |
35+
| Variable | Default | Category | Description |
36+
|-------------------------|:-------:|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
37+
| 📌 `BLUESKY_INSTANCE` || ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky instance the account is registered on.<br/>_(eg: `bsky.social`)_ |
38+
| 📌 `BLUESKY_IDENTIFIER` || ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky user identifier.<br/>_(eg: `handle.bsky.social`)_ |
39+
| 📌 `BLUESKY_PASSWORD` || ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky password.<br/>_(Can be a user password or an [app password](https://bsky.app/settings/app-passwords))_ |
3940

4041
## Synchronization 🐝
4142

4243
Configure here how the sync will be done.
4344

44-
| Variable | Default | Category | Description |
45-
|----------------------------|:-------:|--------------------|---------------------------------------------|
46-
| 📌 `SYNC_MASTODON` | false | **Sync**::platform | Whether run the sync to Mastodon. |
47-
| 📌 `SYNC_BLUESKY` | false | **Sync**::platform | Whether run the sync to Bluesky. |
48-
| `SYNC_PROFILE_NAME` | false | **Sync**::profile | Whether sync the profile name. |
49-
| `SYNC_PROFILE_DESCRIPTION` | false | **Sync**::profile | Whether sync the profile description. |
50-
| `SYNC_PROFILE_PICTURE` | false | **Sync**::profile | Whether sync the profile picture. |
51-
| `SYNC_PROFILE_HEADER` | false | **Sync**::profile | Whether sync the profile header (= banner). |
45+
| Variable | Default | Category | Description |
46+
|----------------------------|:-------:|--------------------------------------------------------------------------|---------------------------------------------|
47+
| 📌 `SYNC_MASTODON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | Whether run the sync to Mastodon. |
48+
| 📌 `SYNC_BLUESKY` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | Whether run the sync to Bluesky. | |
49+
| `SYNC_PROFILE_NAME` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile name. |
50+
| `SYNC_PROFILE_DESCRIPTION` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile description. |
51+
| `SYNC_PROFILE_PICTURE` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile picture. |
52+
| `SYNC_PROFILE_HEADER` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile header (= banner). |
5253

5354
## Configuration with Docker 🐳
5455

56+
| Variable | Default | Category | Description |
57+
|-------------------------|:-------:|--------------------------------------------------------------------------|------------------------------------------------|
58+
| 📌 `SYNC_FREQUENCY_MIN` | 30 | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | At which frequency run the sync. |
59+
| 📌 `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | 👀Set it to **true** to synchronize regularly. |
60+
61+
62+
5563
## Configuration with Cron ⏰
5664

65+
| Variable | Default | Category | Description |
66+
|-------------|:-------:|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------|
67+
| 📌 `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | ⚠️ Set it to **false**. Without this, you'll get multiple instances running without end. |
68+
5769
## Configuration with PM2 ⏲️
5870

59-
| Variable | Default | Category | Description |
60-
|:----------------:|:----------:|------------------|-------------------------------------------------------------------------------------------------------|
61-
| 📌 `EXECUTION` | | **PM2**::runtime | The execution type you want to rely on (if you use the `deploy.sh` script). Value **has to be 'pm2'** |
62-
| `INSTANCE_ID` | 'instance' | **PM2**::runtime | The pm2 instance name suffix. Will generate `touitomamout-[INSTANCE_ID]`. |
71+
| Variable | Default | Category | Description |
72+
|-------------------------|:-------:|--------------------------------------------------------------------------|----------------------------------|
73+
| 📌 `SYNC_FREQUENCY_MIN` | 30 | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | At which frequency run the sync. |
74+
| `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | ⚠️ Set it to **false**. |
6375

6476
## Configuration with Manual execution 👏️
6577

66-
| Variable | Default | Category | Description |
67-
|:----------------:|:-------:|---------------------|----------------------------------------------------------------------------------------------------------|
68-
| 📌 `EXECUTION` || **Manual**::runtime | The execution type you want to rely on (if you use the `deploy.sh` script). Value **has to be 'manual'** |
78+
| Variable | Default | Category | Description |
79+
|----------------------|:-------:|--------------------------------------------------------------------------|-----------------------------------------------------------------------------|
80+
| `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | Set it to **false** for a one shot sync, to **true** to make it run again |
81+
| `SYNC_FREQUENCY_MIN` | 30 | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | At which frequency run the sync. Only apply if you set `DAEMON` to **true** |
82+

‎docs/docs/resources/storage/auth-system.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A `Cookies` file is always named with the following naming: `cookies.<source-twi
1111
## Cookies file location
1212
The cookies file is stored in at the root level of the project.
1313

14-
## Cache file format
14+
## Cookies file format
1515
Here is an example of a cache file:
1616
```json
1717
[

‎docs/docs/resources/storage/cache-system.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Here is an example of a cache file:
2121
"rkey": "<corresponding-bluesky-post-rkey"
2222
}
2323
}
24-
}
24+
},
25+
"version": "x.x"
2526
}
2627
```

‎docs/sidebars.js

+9-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313

1414
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
1515
const sidebars = {
16-
// By default, Docusaurus generates a sidebar from the docs folder structure
17-
rootSidebar: [{
18-
type: 'autogenerated',
19-
dirName: '.'
20-
}]
21-
16+
// By default, Docusaurus generates a sidebar from the docs folder structure
17+
rootSidebar: [
18+
{
19+
type: "autogenerated",
20+
dirName: ".",
21+
},
22+
],
2223

23-
// But you can create a sidebar manually
24-
/*
24+
// But you can create a sidebar manually
25+
/*
2526
tutorialSidebar: [
2627
'intro',
2728
'hello',

‎docs/src/components/environment-generator/fields.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,35 @@ import React from "react";
33

44
import { getFormattedLabel } from "./utils";
55

6-
const TextComponent = ({ categorySlug, field, onChange }) => {
6+
const TextComponent = ({
7+
categorySlug,
8+
field,
9+
onChange,
10+
type = "text" || "number",
11+
}) => {
712
const label = getFormattedLabel(field);
813

914
return (
1015
<>
11-
<Text size={"2"}>{label}</Text>
16+
<Text size="2">{label}</Text>
1217
<TextField.Input
1318
radius="large"
1419
variant="surface"
20+
type={type}
1521
placeholder={label}
1622
onChange={(e) => onChange(categorySlug, field.name, e.target.value)}
1723
value={field.value}
1824
/>
1925
</>
2026
);
2127
};
28+
2229
const ToggleComponent = ({ category, categorySlug, field, onChange }) => {
2330
const label = getFormattedLabel(field);
2431

2532
return (
26-
<Flex gap="2" justify="between">
27-
<Text size={"2"}>{label}</Text>
33+
<Flex gap="2" justify="between" style={{ paddingBottom: "8px" }}>
34+
<Text size="2">{label}</Text>
2835
<Switch
2936
color="cyan"
3037
radius="large"
@@ -41,7 +48,7 @@ const SelectComponent = ({ categorySlug, field, onChange }) => {
4148

4249
return (
4350
<Flex gap="2" direction="column">
44-
<Text size={"2"}>{label}</Text>
51+
<Text size="2">{label}</Text>
4552
<Select.Root
4653
value={field.value}
4754
onValueChange={(selectedValue) =>

‎docs/src/components/environment-generator/generator.js

+105-96
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ const twitterSourceWarning = () => (
3030
</Link>
3131
</>
3232
);
33+
const daemonWarning = () => (
34+
<>
35+
The daemon mode should only be enabled when running Touitomamout in a Docker
36+
container or when run as a "manual sync". It is <i>enabling</i> the self
37+
restarting synchronization loop. <br />
38+
<span
39+
style={{
40+
display: "block",
41+
marginTop: "8px",
42+
paddingLeft: "8px",
43+
borderLeft: "2px solid #c4c4c4",
44+
}}
45+
>
46+
If you rely on a <b>cron</b> or <b>pm2</b>, please set it to false or
47+
remove the environment variable.
48+
</span>
49+
</>
50+
);
3351

3452
const Generator = ({ setConfiguration }) => {
3553
const [data, setData] = useState({
@@ -106,8 +124,29 @@ const Generator = ({ setConfiguration }) => {
106124
},
107125
],
108126
},
127+
sync: {
128+
name: "🔄 Synchronization",
129+
required: false,
130+
fields: [
131+
{
132+
name: "daemon",
133+
warning: daemonWarning,
134+
label: "Daemon mode 😈 (only for Docker or manual sync)",
135+
type: "boolean",
136+
value: false,
137+
env: "DAEMON",
138+
},
139+
{
140+
name: "frequency",
141+
label: "Synchronization frequency in minutes",
142+
type: "number",
143+
value: 30,
144+
env: "SYNC_FREQUENCY_MIN",
145+
},
146+
],
147+
},
109148
profile: {
110-
name: "🔄 Profile synchronization",
149+
name: "👻 Profile synchronization",
111150
required: false,
112151
fields: [
113152
{
@@ -140,48 +179,6 @@ const Generator = ({ setConfiguration }) => {
140179
},
141180
],
142181
},
143-
runtime: {
144-
name: "⚙️ Runtime configuration",
145-
required: true,
146-
fields: [
147-
{
148-
name: "instance",
149-
label: "Touitomamout instance id",
150-
type: "string",
151-
value: "",
152-
env: "INSTANCE_ID",
153-
},
154-
{
155-
name: "execution",
156-
label: "Execution method",
157-
type: "select",
158-
value: "",
159-
env: "EXECUTION",
160-
validationHandler: (value) => {
161-
const keepValues = ["pm2", "manual"];
162-
return keepValues.includes(value);
163-
},
164-
options: [
165-
{
166-
label: "PM2",
167-
value: "pm2",
168-
},
169-
{
170-
label: "Manual",
171-
value: "manual",
172-
},
173-
{
174-
label: "Docker",
175-
value: "docker",
176-
},
177-
{
178-
label: "Cron",
179-
value: "cron",
180-
},
181-
],
182-
},
183-
],
184-
},
185182
});
186183

187184
const buildConfigurationFile = () => {
@@ -263,80 +260,92 @@ const Generator = ({ setConfiguration }) => {
263260
<Text>Create your configuration in a few clicks!</Text>
264261
<Separator my="3" size="4" />
265262

266-
{Object.entries(data).map(([categorySlug, category]) => (
267-
<Flex gap="3" direction="column" key={categorySlug}>
268-
<Flex gap="2" justify="between">
269-
<Heading size="3" as="h2">
270-
{category.name}
271-
</Heading>
272-
{typeof category.enabled === "boolean" ? (
273-
<Switch
274-
color="cyan"
275-
checked={category.enabled}
276-
onCheckedChange={(enabled) =>
277-
onCategoryToggleChange(categorySlug, enabled)
278-
}
279-
/>
263+
{Object.entries(data).map(
264+
([categorySlug, category], index, entries) => (
265+
<Flex gap="3" direction="column" key={categorySlug}>
266+
<Flex gap="2" justify="between">
267+
<Heading size="3" as="h2">
268+
{category.name}
269+
</Heading>
270+
{typeof category.enabled === "boolean" ? (
271+
<Switch
272+
color="cyan"
273+
checked={category.enabled}
274+
onCheckedChange={(enabled) =>
275+
onCategoryToggleChange(categorySlug, enabled)
276+
}
277+
/>
278+
) : null}
279+
</Flex>
280+
281+
{category.description ? (
282+
<Text>Create your configuration in a few clicks!</Text>
280283
) : null}
281-
</Flex>
282284

283-
{category.description ? (
284-
<Text>Create your configuration in a few clicks!</Text>
285-
) : null}
285+
{category.warning ? (
286+
<Callout.Root color="gray">
287+
<Callout.Icon>
288+
<InfoCircledIcon />
289+
</Callout.Icon>
290+
<Callout.Text>
291+
<category.warning />
292+
</Callout.Text>
293+
</Callout.Root>
294+
) : null}
286295

287-
{category.warning ? (
288-
<Callout.Root color="gray">
289-
<Callout.Icon>
290-
<InfoCircledIcon />
291-
</Callout.Icon>
292-
<Callout.Text>
293-
<category.warning />
294-
</Callout.Text>
295-
</Callout.Root>
296-
) : null}
296+
{category.enabled !== false
297+
? category.fields.map((field) => (
298+
<div key={categorySlug + field.name}>
299+
{field.warning ? (
300+
<Callout.Root
301+
color="gray"
302+
size="1"
303+
style={{ marginBottom: "8px" }}
304+
>
305+
<Callout.Icon>
306+
<InfoCircledIcon />
307+
</Callout.Icon>
308+
<Callout.Text>
309+
<field.warning />
310+
</Callout.Text>
311+
</Callout.Root>
312+
) : null}
297313

298-
{category.enabled !== false
299-
? category.fields.map((field) => {
300-
if (field.type === "boolean") {
301-
return (
302-
<div key={categorySlug + field.name}>
314+
{field.type === "boolean" ? (
303315
<ToggleComponent
304316
field={field}
305317
categorySlug={categorySlug}
306318
category={category}
307319
onChange={onFieldChange}
308320
/>
309-
</div>
310-
);
311-
}
312-
if (field.type === "string") {
313-
return (
314-
<div key={categorySlug + field.name}>
321+
) : null}
322+
323+
{field.type === "string" || field.type === "number" ? (
315324
<TextComponent
316325
field={field}
326+
type={field.type}
317327
categorySlug={categorySlug}
318328
onChange={onFieldChange}
319329
/>
320-
</div>
321-
);
322-
}
323-
if (field.type === "select") {
324-
return (
325-
<div key={categorySlug + field.name}>
330+
) : null}
331+
332+
{field.type === "select" ? (
326333
<SelectComponent
327334
field={field}
328335
categorySlug={categorySlug}
329336
onChange={onFieldChange}
330337
/>
331-
</div>
332-
);
333-
}
334-
})
335-
: null}
338+
) : null}
339+
</div>
340+
))
341+
: null}
336342

337-
<Separator my="3" size="4" />
338-
</Flex>
339-
))}
343+
{index !== entries.length - 1 ? (
344+
<Separator my="3" size="4" />
345+
) : null}
346+
</Flex>
347+
),
348+
)}
340349
</Flex>
341350
</Card>
342351
);
+52-43
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,67 @@
1-
import Link from '@docusaurus/Link';
2-
import clsx from 'clsx';
3-
import PropTypes from 'prop-types';
4-
import React from 'react';
1+
import Link from "@docusaurus/Link";
2+
import clsx from "clsx";
3+
import PropTypes from "prop-types";
4+
import React from "react";
55

6-
import styles from './styles.module.css';
6+
import styles from "./styles.module.css";
77

88
const PlatformList = [
9-
{
10-
name: 'Docker',
11-
icon: '🐳',
12-
},
13-
{
14-
name: 'Cron',
15-
icon: '⏰',
16-
},
17-
{
18-
name: 'PM2',
19-
icon: '⏲️',
20-
},
21-
{
22-
name: 'Manual sync',
23-
icon: '👏️',
24-
},
9+
{
10+
name: "Docker",
11+
icon: "🐳",
12+
},
13+
{
14+
name: "Cron",
15+
icon: "⏰",
16+
},
17+
{
18+
name: "PM2",
19+
icon: "⏲️",
20+
},
21+
{
22+
name: "Manual sync",
23+
icon: "👏️",
24+
},
2525
];
2626

2727
Platform.propTypes = {
28-
icon: PropTypes.string.isRequired,
29-
name: PropTypes.string.isRequired,
28+
icon: PropTypes.string.isRequired,
29+
name: PropTypes.string.isRequired,
3030
};
3131

3232
PlatformList.propTypes = Array.from(Platform.propTypes);
3333

3434
function Platform({ icon, name }) {
35-
const page = name.toLowerCase().replaceAll(' ', '-');
36-
return (
37-
<Link href={`/docs/configuration/${page}`} className={clsx('col col--5', styles.platformLink)}>
38-
<div className={styles.platformLabel}>
39-
<span className={styles.platformIcon} role="img" aria-hidden={true}>{icon}</span>
40-
<h3 className={styles.platformName}>Configure with<br/><span className={styles.platformNameSpan}>{name}</span></h3>
41-
</div>
42-
</Link>
43-
);
35+
const page = name.toLowerCase().replaceAll(" ", "-");
36+
return (
37+
<Link
38+
href={`/docs/configuration/${page}`}
39+
className={clsx("col col--5", styles.platformLink)}
40+
>
41+
<div className={styles.platformLabel}>
42+
<span className={styles.platformIcon} role="img" aria-hidden={true}>
43+
{icon}
44+
</span>
45+
<h3 className={styles.platformName}>
46+
Configure with
47+
<br />
48+
<span className={styles.platformNameSpan}>{name}</span>
49+
</h3>
50+
</div>
51+
</Link>
52+
);
4453
}
4554

4655
export default function Index() {
47-
return (
48-
<section>
49-
<div className="container">
50-
<div className={clsx(['row',styles.platformsWrapper])}>
51-
{PlatformList.map((props) => (
52-
<Platform key={props.name} {...props} />
53-
))}
54-
</div>
55-
</div>
56-
</section>
57-
);
56+
return (
57+
<section>
58+
<div className="container">
59+
<div className={clsx(["row", styles.platformsWrapper])}>
60+
{PlatformList.map((props) => (
61+
<Platform key={props.name} {...props} />
62+
))}
63+
</div>
64+
</div>
65+
</section>
66+
);
5867
}

‎docs/src/css/custom.css

+4
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@
3030
--ifm-color-primary-lightest: #4fbcdd;
3131
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
3232
}
33+
34+
table {
35+
width:100%;
36+
}

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"test": "jest --config ./meta/testing/jest.config.cjs",
99
"build": "tsc",
1010
"predeploy": "npm ci && npm run build",
11-
"deploy": "bash ./deployment/deploy.sh",
12-
"deploy:update": "bash ./deployment/deploy.sh --update",
11+
"pm2": "bash deployment/pm2.sh",
12+
"pm2:update": "bash deployment/pm2.sh --update",
1313
"postinstall": "husky install .husky",
1414
"commitlint": "commitlint --edit"
1515
},

‎src/constants.ts

+27-16
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,38 @@ if (!envAvailable) {
1616
throw new Error("No suitable .env file found.");
1717
}
1818

19+
const trimTwitterHandle = (handle: string) => {
20+
return handle.toLowerCase().trim().replaceAll("@", "");
21+
};
22+
1923
dotenv.config({ path: envPath });
2024

21-
export const TWITTER_HANDLE = process.env.TWITTER_HANDLE || "";
22-
export const TWITTER_USERNAME = process.env.TWITTER_USERNAME || "";
23-
export const TWITTER_PASSWORD = process.env.TWITTER_PASSWORD || "";
24-
export const MASTODON_INSTANCE = process.env.MASTODON_INSTANCE || "";
25-
export const MASTODON_ACCESS_TOKEN = process.env.MASTODON_ACCESS_TOKEN || "";
26-
export const BLUESKY_INSTANCE = process.env.BLUESKY_INSTANCE || "";
27-
export const BLUESKY_IDENTIFIER = process.env.BLUESKY_IDENTIFIER || "";
28-
export const BLUESKY_PASSWORD = process.env.BLUESKY_PASSWORD || "";
29-
export const INSTANCE_ID = process.env.INSTANCE_ID ?? "instance";
30-
export const STORAGE_DIR = process.env.STORAGE_DIR ?? process.cwd();
31-
export const CACHE_PATH = `${STORAGE_DIR}/cache.${INSTANCE_ID.toLowerCase()
25+
export const TWITTER_HANDLE = trimTwitterHandle(
26+
process.env.TWITTER_HANDLE || "",
27+
);
28+
export const TWITTER_USERNAME = trimTwitterHandle(
29+
process.env.TWITTER_USERNAME || "",
30+
);
31+
export const TWITTER_PASSWORD = (process.env.TWITTER_PASSWORD || "").trim();
32+
export const MASTODON_INSTANCE = (process.env.MASTODON_INSTANCE || "").trim();
33+
export const MASTODON_ACCESS_TOKEN = (
34+
process.env.MASTODON_ACCESS_TOKEN || ""
35+
).trim();
36+
export const BLUESKY_INSTANCE = (process.env.BLUESKY_INSTANCE || "").trim();
37+
export const BLUESKY_IDENTIFIER = (process.env.BLUESKY_IDENTIFIER || "").trim();
38+
export const BLUESKY_PASSWORD = (process.env.BLUESKY_PASSWORD || "").trim();
39+
export const INSTANCE_ID = (TWITTER_HANDLE ?? "instance")
40+
.toLowerCase()
3241
.trim()
33-
.replaceAll(" ", "_")}.json`;
34-
export const COOKIES_PATH = `${STORAGE_DIR}/cookies.${INSTANCE_ID.toLowerCase()
35-
.trim()
36-
.replaceAll(" ", "_")}.json`;
42+
.replaceAll(" ", "_");
43+
export const STORAGE_DIR = process.env.STORAGE_DIR ?? process.cwd();
44+
export const CACHE_PATH = `${STORAGE_DIR}/cache.${INSTANCE_ID}.json`;
45+
export const COOKIES_PATH = `${STORAGE_DIR}/cookies.${INSTANCE_ID}.json`;
3746
export const SYNC_MASTODON = (process.env.SYNC_MASTODON || "false") === "true";
3847
export const SYNC_BLUESKY = (process.env.SYNC_BLUESKY || "false") === "true";
48+
export const SYNC_FREQUENCY_MIN = parseInt(
49+
process.env.SYNC_FREQUENCY_MIN || "30",
50+
);
3951
export const SYNC_PROFILE_DESCRIPTION =
4052
(process.env.SYNC_PROFILE_DESCRIPTION || "false") === "true";
4153
export const SYNC_PROFILE_PICTURE =
@@ -46,6 +58,5 @@ export const SYNC_PROFILE_HEADER =
4658
(process.env.SYNC_PROFILE_HEADER || "false") === "true";
4759
export const DEBUG = (process.env.TOUITOMAMOUT_DEBUG || "false") === "true";
4860
export const DAEMON = (process.env.DAEMON || "false") === "true";
49-
export const DAEMON_PERIOD_MIN = parseInt(process.env.DAEMON_PERIOD_MIN ?? "7"); // Default 7 min
5061
export const VOID = "∅";
5162
export const API_RATE_LIMIT = parseInt(process.env.API_RATE_LIMIT ?? "10");

‎src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { configuration } from "./configuration/configuration.js";
22
import {
33
DAEMON,
4-
DAEMON_PERIOD_MIN,
54
SYNC_BLUESKY,
5+
SYNC_FREQUENCY_MIN,
66
SYNC_MASTODON,
77
TWITTER_HANDLE,
88
} from "./constants.js";
@@ -64,11 +64,11 @@ const touitomamout = async () => {
6464
await touitomamout();
6565

6666
if (DAEMON) {
67-
console.log(`Run daemon every ${DAEMON_PERIOD_MIN}min`);
67+
console.log(`Run daemon every ${SYNC_FREQUENCY_MIN}min`);
6868
setInterval(
6969
async () => {
7070
await touitomamout();
7171
},
72-
DAEMON_PERIOD_MIN * 60 * 1000,
72+
SYNC_FREQUENCY_MIN * 60 * 1000,
7373
);
7474
}

0 commit comments

Comments
 (0)
This repository has been archived.