diff --git a/.env.example b/.env.example index e58d7a0..aa735de 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -// Data source +# Data source TWITTER_HANDLE=username -// API +# API TWITTER_USERNAME=username TWITTER_PASSWORD=password MASTODON_INSTANCE=mastodon.social @@ -10,12 +10,10 @@ BLUESKY_INSTANCE= BLUESKY_IDENTIFIER= BLUESKY_PASSWORD= -// Touitomamout -INSTANCE_ID= -EXECUTION=manual -# EXECUTION=pm2 +# Touitomamout SYNC_MASTODON=true SYNC_BLUESKY=true +SYNC_FREQUENCY_MIN=30 SYNC_PROFILE_DESCRIPTION=false SYNC_PROFILE_PICTURE=false SYNC_PROFILE_HEADER=false diff --git a/README.md b/README.md index 8e5c0ed..e7e778a 100644 --- a/README.md +++ b/README.md @@ -27,92 +27,9 @@ Please find the project documentation here: [](https://louisgrasset.github.io/touitomamout/docs/discover) +## Dependencies +Kudos to the following projects that made Touitomamout project possible 🙏 +- 🦤 [twitter-scraper](https://github.com/the-convocation/twitter-scraper) +- 🦣 [masto.js](https://github.com/neet/masto.js) +- ☁️[atproto](https://github.com/bluesky-social/atproto) -## Installation -**Clone** the project -```bash -git clone git@github.com:louisgrasset/touitomamout.git -``` - -**Install** dependencies & build the project -```bash -npm ci && npm run build -``` -## Configuration -Touitomamout relies on two APIs: -- [Mastodon](https://docs.joinmastodon.org/client/intro/) -- [Twitter-Scraper](https://github.com/the-convocation/twitter-scraper) - -### Mastodon configuration -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. -1. Go to your account's application page: `https://{yourinstance.tld}/settings/applications/new` -2. Create a new application with the following scopes: -- `read:accounts`: get your mastodon account username -- `write:media`: post medias -- `write:statuses`: post toots -- `write:accounts`: update your profile -3. Populate the [Environment](#Environment) section with your `access token`. - -### Twitter configuration -The tweets retrieval by itself can be done without Twitter credentials. But keep in mind that twitter currently blocks guests to access users' replies. -Touitomamout is trying to restore the previous session, so you'll not get spammed by the Twitter security team for each connection. - - -> **Note** -> -> The configuration allows you to sync a first account and authenticate with a secondary account for two reasons: -> 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. -> 2. Because this project is running with a non-official API, you may not want to put your account at risk. - - -### Environment -First things first, please copy the [`.env.example`](https://github.com/louisgrasset/touitomamout/blob/main/.env.example) file to `.env`. -Then, please fill each variable. - -> **Warning** -> -> Do not forget to properly choose the `EXECUTION` variable. -> Two values are allowed: -> 1. `manual`: a simple node script execution -> 2. `pm2`: spawns as a new PM2 process, named with `touitomamout-${instance_id}` pattern. - - -### Multiple instances -This project supports a multiple instances mode. To do so, simply provide multiple `.env` files such as `.env.instance1` and `.env.instance2`. - -`deploy` & `deploy:update` scripts will handle them properly. - -## Run it - -### Manually -You can simply run a `node ./dist/index.js .env` and have a one shot sync of your recent tweets. - -> **Note** -> -> Don't forget to replace `.env` with the right .env filename. - - -### Cron -Because automation is cool, feel free to run that script everytime you need to. -Simply create your cron [here](https://crontab.guru). - -### PM2 support -[PM2](https://pm2.keymetrics.io/) is a utility that allows you to monitor and run periodically your node scripts. -Thus, it can be useful for some users to deploy Touitomamout to a PM2 instance. - -#### **PM2**: First run -```bash -npm run deploy -``` - -#### **PM2**: Update your instance after a code update -Your instance will be removed and will be generated again with the latest codebase. -Your `cache.instance.json` file is kept, so you won't have duplicated toots. -```bash -npm ci && -npm run build && -npm run deploy:update -``` - -### Docker -You can alternatively rely on docker and use the `docker-compose.yml` file. diff --git a/deployment/deploy.sh b/deployment/deploy.sh deleted file mode 100644 index 746a7b5..0000000 --- a/deployment/deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh - -script_path=$(realpath "$0") -script_dir=$(dirname "$script_path") - -touitomamout="$script_dir/../dist/index.js" - -# For each env file found (except .env.example) -for env in .env*; do - if [[ $env != *.example ]]; then - # Extract the variable from the env file - name=$(grep "INSTANCE_ID=" "$env" | cut -d '=' -f 2) - execution=$(grep "EXECUTION=" "$env" | cut -d '=' -f 2) - - if [[ $execution == "manual" ]]; then - node $touitomamout "$env" - elif [[ $execution == "pm2" ]]; then - if [[ $1 == "--update" ]]; then - pm2 del touitomamout-"$name" - fi - pm2 start "$touitomamout" --name touitomamout-"$name" --no-autorestart --cron '*/7 * * * *' -- "$env" - pm2 save - else - echo "Missing execution settings.\n | Please check your '$env' file.\n | EXECUTION value found: '$execution'" - fi - fi -done diff --git a/deployment/pm2.sh b/deployment/pm2.sh new file mode 100644 index 0000000..40b2f3e --- /dev/null +++ b/deployment/pm2.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +script_path=$(realpath "$0") +script_dir=$(dirname "$script_path") + +touitomamout="$script_dir/../dist/index.js" + +# For each env file found (except .env.example) +for env in .env*; do + if [[ $env != *.example ]]; then + # Prevent startup with PM2 if the internal DAEMON is set to true. PM2 will handle the cron itself. + daemon=$(grep "DAEMON=" "$env" | cut -d '=' -f 2) + + if [[ daemon == "true" ]]; then + echo "DAEMON is enabled. Disable it before deploying Touitomamout with PM2. Please check your .env file." + exit 1 + fi + + # Handle the instance name + name=$(grep "TWITTER_HANDLE=" "$env" | cut -d '=' -f 2) + + if [[ -z $name ]]; then + echo "No value found for TWITTER_HANDLE in $env. Please check your .env file." + exit 1 + fi + + # Handle the sync frequency + frequency=$(grep "SYNC_FREQUENCY_MIN=" "$env" | cut -d '=' -f 2) + + if [[ -z $frequency ]]; then + echo "No value found for SYNC_FREQUENCY_MIN in $env. Using default value. Please check your .env file." + frequency=30 + fi + + # If called with --update flag, delete the instance before starting it again + if [[ $1 == "--update" ]]; then + pm2 del touitomamout-"$name" + fi + + pm2 start "$touitomamout" --name touitomamout-"$name" --no-autorestart --cron "*/$frequency * * * *" -- "$env" + pm2 save + fi +done diff --git a/docker-compose.source.yml b/docker-compose.source.yml new file mode 100644 index 0000000..e1e67a9 --- /dev/null +++ b/docker-compose.source.yml @@ -0,0 +1,13 @@ +version: '3.9' + +services: + touitomamout: + container_name: "touitomamout" + build: + context: ./ # ← This will build the image from the source code + restart: unless-stopped + environment: + - ENV_FILE=/data/.env + - STORAGE_DIR=/data + volumes: + - ./data:/data diff --git a/docker-compose.yml b/docker-compose.yml index 93dccb4..fab2d4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,11 +2,11 @@ version: '3.9' services: touitomamout: - build: - context: ./ + container_name: "touitomamout" + image: louisgrasset/touitomamout:latest # Or "ghcr.io/louisgrasset/touitomamout:latest" + restart: unless-stopped environment: - ENV_FILE=/data/.env - STORAGE_DIR=/data - - DAEMON_PERIOD_MIN=1 volumes: - ./data:/data diff --git a/docs/docs/configuration/cron.md b/docs/docs/configuration/cron.md deleted file mode 100644 index 0c62502..0000000 --- a/docs/docs/configuration/cron.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -sidebar_position: 3 -title: Configure with Cron -tags: [ - 'configuration', - 'cron', -] ---- - -# ⏰ Cron configuration - -## Define environment variables -3 types of environment variables are required to run the sync: -- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables). -- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-). -- **Cron variables**: required for the cron, get them [here](/docs/resources/environment-variables#configuration-with-cron-). diff --git a/docs/docs/configuration/cron.mdx b/docs/docs/configuration/cron.mdx new file mode 100644 index 0000000..5ffdf18 --- /dev/null +++ b/docs/docs/configuration/cron.mdx @@ -0,0 +1,41 @@ +--- +sidebar_position: 3 +title: Configure with Cron +tags: [ + 'configuration', + 'cron', +] +--- +import SectionPullInstallProject from "./sections/_section_pull_install_project.md" +import TipFileGenerator from "./sections/_tip-file-generator.md" + +# ⏰ Cron configuration + +## Installation + + +## Define environment variables +3 types of environment variables are required to run the sync: +- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables). +- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-). +- **Cron variables**: required for the cron, get them [here](/docs/resources/environment-variables#configuration-with-cron-). + + + +Once your env variables are ready, inject them in a .env.{handle_to_sync} file. (e.g: `.env.signalapp`) + +## Run it! + +> You can create your own cron expression here: [crontab.guru](https://crontab.guru/). + +First, enter the cron editor: +```bash +crontab -e +``` + +Then, add the following line to run the sync every 30 minutes: +```bash +*/30 * * * * cd /home/{user}/services/touitmamout && node ./dist/index.js .env +``` + +Finally, save and exit the editor. diff --git a/docs/docs/configuration/docker.md b/docs/docs/configuration/docker.md deleted file mode 100644 index 34605fb..0000000 --- a/docs/docs/configuration/docker.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -sidebar_position: 2 -title: Configure with Docker -tags: [ - 'configuration', - 'docker', -] ---- - -# 🐳 Docker configuration - -## Define environment variables -3 types of environment variables are required to run the sync: -- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables) -- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-) -- **Docker variables**: required for the docker container, get them [here](/docs/resources/environment-variables#configuration-with-docker-) diff --git a/docs/docs/configuration/docker.mdx b/docs/docs/configuration/docker.mdx new file mode 100644 index 0000000..2ad755a --- /dev/null +++ b/docs/docs/configuration/docker.mdx @@ -0,0 +1,71 @@ +--- +sidebar_position: 2 +title: Configure with Docker +tags: [ + 'configuration', + 'docker', +] +--- +import TipFileGenerator from "./sections/_tip-file-generator.md" + +# 🐳 Docker configuration + +## Define environment variables +3 types of environment variables are required to run the sync: +- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables) +- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-) +- **Docker variables**: required for the docker container, get them [here](/docs/resources/environment-variables#configuration-with-docker-) + + + +## Create the touitomamout docker container +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. + +--- +📦 Releases are published on the following registries: +- [Docker Hub](https://hub.docker.com/r/louisgrasset/touitomamout) (`louisgrasset/touitomamout`) +- [Github (GHCR)](https://github.com/louisgrasset/touitomamout/pkgs/container/touitomamout) (`ghcr.io/louisgrasset/touitomamout`) + +| Image tag | Description | +|-----------|----------------------------------------------------| +| `latest` | Corresponds to the latest release. | +| `dev` | Reflects the most recent changes on "main" branch. | +| `x.x.x` | A specific version. | + +## From the image +```yml title="docker-compose.yml" +version: '3.9' + +services: + touitomamout: + container_name: "touitomamout" + image: louisgrasset/touitomamout:latest # Or "ghcr.io/louisgrasset/touitomamout:latest" + restart: unless-stopped + environment: + - ENV_FILE=/data/.env + - STORAGE_DIR=/data + volumes: + - ./data:/data +``` +## From the source code +```yml title="docker-compose.source.yml" +version: '3.9' + +services: + touitomamout: + container_name: "touitomamout" + build: + context: ./ # ← This will build the image from the source code + restart: unless-stopped + environment: + - ENV_FILE=/data/.env + - STORAGE_DIR=/data + volumes: + - ./data:/data +``` + +## Run it! +Simply run this command to start the container. +```bash +docker-compose up -d +``` diff --git a/docs/docs/configuration/manual-sync.md b/docs/docs/configuration/manual-sync.mdx similarity index 69% rename from docs/docs/configuration/manual-sync.md rename to docs/docs/configuration/manual-sync.mdx index 6453896..af41cf8 100644 --- a/docs/docs/configuration/manual-sync.md +++ b/docs/docs/configuration/manual-sync.mdx @@ -6,11 +6,23 @@ tags: [ 'manual sync', ] --- +import SectionPullInstallProject from "./sections/_section_pull_install_project.md" +import TipFileGenerator from "./sections/_tip-file-generator.md" # 👏️️ Manual sync configuration +## Installation + + ## Define environment variables 3 types of environment variables are required to run the sync: - **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables). - **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-). - **Manual sync variables**: required for the manual sync, get them [here](/docs/resources/environment-variables#configuration-with-manual-sync-). + + + +## Run it! +```bash +node ./dist/index.js .env.{handle_to_sync} +``` diff --git a/docs/docs/configuration/pm2.md b/docs/docs/configuration/pm2.md deleted file mode 100644 index 2760b36..0000000 --- a/docs/docs/configuration/pm2.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -sidebar_position: 4 -title: Configure with PM2 -tags: [ - 'configuration', - 'pm2', -] ---- - -# ⏲️ PM2 configuration - -## Define environment variables -3 types of environment variables are required to run the sync: -- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables). -- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-). -- **PM2 variables**: required for the PM2 instance, get them [here](/docs/resources/environment-variables#configuration-with-pm2-). diff --git a/docs/docs/configuration/pm2.mdx b/docs/docs/configuration/pm2.mdx new file mode 100644 index 0000000..2a8166d --- /dev/null +++ b/docs/docs/configuration/pm2.mdx @@ -0,0 +1,41 @@ +--- +sidebar_position: 4 +title: Configure with PM2 +tags: [ + 'configuration', + 'pm2', +] +--- +import SectionPullInstallProject from "./sections/_section_pull_install_project.md" +import TipFileGenerator from "./sections/_tip-file-generator.md" + +# ⏲️ PM2 configuration + +## Installation + + +## Define environment variables +3 types of environment variables are required to run the sync: +- **Core variables**: required in situations, get them [here](/docs/resources/environment-variables#core-variables). +- **Sync variables**: required for the sync **you** want, get them [here](/docs/resources/environment-variables#synchronization-). +- **PM2 variables**: required for the PM2 instance, get them [here](/docs/resources/environment-variables#configuration-with-pm2-). + + + +Once your env variables are ready, inject them in a .env.{handle_to_sync} file. (e.g: `.env.signalapp`) + +## Run it! + +Touitomamout comes with a script that will automatically handle the PM2 instances management for you. + +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. + +```bash +npm run pm2 +``` + +:::info I upgraded the project to the latest version, what should I do? +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: +```bash +npm run pm2:update +``` diff --git a/docs/docs/configuration/sections/_section_pull_install_project.md b/docs/docs/configuration/sections/_section_pull_install_project.md new file mode 100644 index 0000000..34d376c --- /dev/null +++ b/docs/docs/configuration/sections/_section_pull_install_project.md @@ -0,0 +1,11 @@ +Assuming you have a folder /home/{user}/services + +**Clone** the project +```bash +cd /home/{user}/services && git clone https://github.com/louisgrasset/touitomamout.git +``` + +**Install** dependencies & build the project +```bash +npm ci && npm run build +``` diff --git a/docs/docs/configuration/sections/_tip-file-generator.md b/docs/docs/configuration/sections/_tip-file-generator.md new file mode 100644 index 0000000..e7cd31d --- /dev/null +++ b/docs/docs/configuration/sections/_tip-file-generator.md @@ -0,0 +1,4 @@ +:::tip Environment file generator + +Feel free to generate your `.env` file with the [generator](/docs/tools/env-generator) +::: diff --git a/docs/docs/resources/_category_.json b/docs/docs/resources/_category_.json index b19057f..7b30ee1 100644 --- a/docs/docs/resources/_category_.json +++ b/docs/docs/resources/_category_.json @@ -1,7 +1,7 @@ { "label": "Resources", "position": 3, - "collapsed": true, + "collapsed": false, "link": { "type": "generated-index" } diff --git a/docs/docs/resources/credentials.md b/docs/docs/resources/credentials.md new file mode 100644 index 0000000..93a5869 --- /dev/null +++ b/docs/docs/resources/credentials.md @@ -0,0 +1,25 @@ +--- +title: Credentials +--- + +# Credentials + +To authenticate with the different platforms, you have to provide credentials. You'll find here the list of all the credentials by platform. + +## Mastodon 🦣 +To communicate with the mastodon instance, you'll have to generate an API Token. It is totally free. + +:::info Application name +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. +In this case : `Touitomamout`. +::: + +1. Go to your account's application page: `https://{yourinstance.tld}/settings/applications/new` +2. Create a new application with the following scopes: + - `read:accounts`: get your mastodon account username + - `write:media`: post medias + - `write:statuses`: post toots + - `write:accounts`: update your profile +3. Copy the token and write it in the .env file as `MASTODON_ACCESS_TOKEN`. + +## Bluesky ☁️ diff --git a/docs/docs/resources/environment-variables.md b/docs/docs/resources/environment-variables.md index d8200c4..1cd8a32 100644 --- a/docs/docs/resources/environment-variables.md +++ b/docs/docs/resources/environment-variables.md @@ -9,11 +9,12 @@ environment variables by usage. `(📌 means the variable is required for the co ## Core variables -| Variable | Default | Category | Description | -|---------------------|:-------:|-------------------|------------------------------------------------| -| `TWITTER_HANDLE` 📌 | ∅ | **Twitter**::data | The twitter username you want to sync. | -| `TWITTER_USERNAME` | ∅ | **Twitter**::auth | The twitter username used to log into twitter. | -| `TWITTER_PASSWORD` | ∅ | **Twitter**::auth | The twitter password used to log into twitter. | +| Variable | Default | Category | Description | +|---------------------|:------------------:|-------------------|--------------------------------------------------------------------------------------------| +| `TWITTER_HANDLE` 📌 | ∅ | **Twitter**::data | The twitter username you want to sync. | +| `TWITTER_USERNAME` | ∅ | **Twitter**::auth | The twitter username used to log into twitter. | +| `TWITTER_PASSWORD` | ∅ | **Twitter**::auth | The twitter password used to log into twitter. | + :::caution Twitter auth @@ -24,45 +25,58 @@ Even if `TWITTER_USERNAME` & `TWITTER_PASSWORD` are optional, these variables ar ## To sync to Mastodon 🦣 -| Variable | Default | Category | Description | -|----------------------------|:-------:|--------------------|---------------------------------------------------------------| -| 📌 `MASTODON_INSTANCE` | ∅ | **Mastodon**::auth | The mastodon instance the account is registered on. | -| 📌 `MASTODON_ACCESS_TOKEN` | ∅ | **Mastodon**::auth | The mastodon access token to authenticated the sync requests. | +| Variable | Default | Category | Description | +|----------------------------|:-------:|--------------------------------------------------------------------------|---------------------------------------------------------------| +| 📌 `MASTODON_INSTANCE` | ∅ | ![**Mastodon**::auth](https://img.shields.io/badge/Mastodon-Auth-6364FF) | The mastodon instance the account is registered on. | +| 📌 `MASTODON_ACCESS_TOKEN` | ∅ | ![**Mastodon**::auth](https://img.shields.io/badge/Mastodon-Auth-6364FF) | The mastodon access token to authenticated the sync requests. | ## To sync to Bluesky ☁️ -| Variable | Default | Category | Description | -|-------------------------|:-------:|-------------------|--------------------------------------------------------------------------------------------------------------------| -| 📌 `BLUESKY_INSTANCE` | ∅ | **Bluesky**::auth | The bluesky instance the account is registered on.
_(eg: `bsky.social`)_ | -| 📌 `BLUESKY_IDENTIFIER` | ∅ | **Bluesky**::auth | The bluesky user identifier.
_(eg: `handle.bsky.social`)_ | -| 📌 `BLUESKY_PASSWORD` | ∅ | **Bluesky**::auth | The bluesky password.
_(Can be a user password or an [app password](https://bsky.app/settings/app-passwords))_ | +| Variable | Default | Category | Description | +|-------------------------|:-------:|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| 📌 `BLUESKY_INSTANCE` | ∅ | ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky instance the account is registered on.
_(eg: `bsky.social`)_ | +| 📌 `BLUESKY_IDENTIFIER` | ∅ | ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky user identifier.
_(eg: `handle.bsky.social`)_ | +| 📌 `BLUESKY_PASSWORD` | ∅ | ![**Bluesky**::auth](https://img.shields.io/badge/Bluesky-Auth-0085ff) | The bluesky password.
_(Can be a user password or an [app password](https://bsky.app/settings/app-passwords))_ | ## Synchronization 🐝 Configure here how the sync will be done. -| Variable | Default | Category | Description | -|----------------------------|:-------:|--------------------|---------------------------------------------| -| 📌 `SYNC_MASTODON` | false | **Sync**::platform | Whether run the sync to Mastodon. | -| 📌 `SYNC_BLUESKY` | false | **Sync**::platform | Whether run the sync to Bluesky. | -| `SYNC_PROFILE_NAME` | false | **Sync**::profile | Whether sync the profile name. | -| `SYNC_PROFILE_DESCRIPTION` | false | **Sync**::profile | Whether sync the profile description. | -| `SYNC_PROFILE_PICTURE` | false | **Sync**::profile | Whether sync the profile picture. | -| `SYNC_PROFILE_HEADER` | false | **Sync**::profile | Whether sync the profile header (= banner). | +| Variable | Default | Category | Description | +|----------------------------|:-------:|--------------------------------------------------------------------------|---------------------------------------------| +| 📌 `SYNC_MASTODON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | Whether run the sync to Mastodon. | +| 📌 `SYNC_BLUESKY` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | Whether run the sync to Bluesky. | | +| `SYNC_PROFILE_NAME` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile name. | +| `SYNC_PROFILE_DESCRIPTION` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile description. | +| `SYNC_PROFILE_PICTURE` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile picture. | +| `SYNC_PROFILE_HEADER` | false | ![**Sync**::profile](https://img.shields.io/badge/Sync-Profile-yellow) | Whether sync the profile header (= banner). | ## Configuration with Docker 🐳 +| Variable | Default | Category | Description | +|-------------------------|:-------:|--------------------------------------------------------------------------|------------------------------------------------| +| 📌 `SYNC_FREQUENCY_MIN` | 30 | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | At which frequency run the sync. | +| 📌 `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | 👀Set it to **true** to synchronize regularly. | + + + ## Configuration with Cron ⏰ +| Variable | Default | Category | Description | +|-------------|:-------:|--------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| 📌 `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. | + ## Configuration with PM2 ⏲️ -| Variable | Default | Category | Description | -|:----------------:|:----------:|------------------|-------------------------------------------------------------------------------------------------------| -| 📌 `EXECUTION` | ∅ | **PM2**::runtime | The execution type you want to rely on (if you use the `deploy.sh` script). Value **has to be 'pm2'** | -| `INSTANCE_ID` | 'instance' | **PM2**::runtime | The pm2 instance name suffix. Will generate `touitomamout-[INSTANCE_ID]`. | +| Variable | Default | Category | Description | +|-------------------------|:-------:|--------------------------------------------------------------------------|----------------------------------| +| 📌 `SYNC_FREQUENCY_MIN` | 30 | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | At which frequency run the sync. | +| `DAEMON` | false | ![**Sync**::platform](https://img.shields.io/badge/Sync-Platform-e8e8e8) | ⚠️ Set it to **false**. | ## Configuration with Manual execution 👏️ -| Variable | Default | Category | Description | -|:----------------:|:-------:|---------------------|----------------------------------------------------------------------------------------------------------| -| 📌 `EXECUTION` | ∅ | **Manual**::runtime | The execution type you want to rely on (if you use the `deploy.sh` script). Value **has to be 'manual'** | +| Variable | Default | Category | Description | +|----------------------|:-------:|--------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| `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 | +| `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** | + diff --git a/docs/docs/resources/storage/auth-system.md b/docs/docs/resources/storage/auth-system.md index 8bf9a80..1afb206 100644 --- a/docs/docs/resources/storage/auth-system.md +++ b/docs/docs/resources/storage/auth-system.md @@ -11,7 +11,7 @@ A `Cookies` file is always named with the following naming: `cookies. { +const TextComponent = ({ + categorySlug, + field, + onChange, + type = "text" || "number", +}) => { const label = getFormattedLabel(field); return ( <> - {label} + {label} onChange(categorySlug, field.name, e.target.value)} value={field.value} @@ -19,12 +25,13 @@ const TextComponent = ({ categorySlug, field, onChange }) => { ); }; + const ToggleComponent = ({ category, categorySlug, field, onChange }) => { const label = getFormattedLabel(field); return ( - - {label} + + {label} { return ( - {label} + {label} diff --git a/docs/src/components/environment-generator/generator.js b/docs/src/components/environment-generator/generator.js index 6146e6c..5c4535d 100644 --- a/docs/src/components/environment-generator/generator.js +++ b/docs/src/components/environment-generator/generator.js @@ -30,6 +30,24 @@ const twitterSourceWarning = () => ( ); +const daemonWarning = () => ( + <> + The daemon mode should only be enabled when running Touitomamout in a Docker + container or when run as a "manual sync". It is enabling the self + restarting synchronization loop.
+ + If you rely on a cron or pm2, please set it to false or + remove the environment variable. + + +); const Generator = ({ setConfiguration }) => { const [data, setData] = useState({ @@ -106,8 +124,29 @@ const Generator = ({ setConfiguration }) => { }, ], }, + sync: { + name: "🔄 Synchronization", + required: false, + fields: [ + { + name: "daemon", + warning: daemonWarning, + label: "Daemon mode 😈 (only for Docker or manual sync)", + type: "boolean", + value: false, + env: "DAEMON", + }, + { + name: "frequency", + label: "Synchronization frequency in minutes", + type: "number", + value: 30, + env: "SYNC_FREQUENCY_MIN", + }, + ], + }, profile: { - name: "🔄 Profile synchronization", + name: "👻 Profile synchronization", required: false, fields: [ { @@ -140,48 +179,6 @@ const Generator = ({ setConfiguration }) => { }, ], }, - runtime: { - name: "⚙️ Runtime configuration", - required: true, - fields: [ - { - name: "instance", - label: "Touitomamout instance id", - type: "string", - value: "", - env: "INSTANCE_ID", - }, - { - name: "execution", - label: "Execution method", - type: "select", - value: "", - env: "EXECUTION", - validationHandler: (value) => { - const keepValues = ["pm2", "manual"]; - return keepValues.includes(value); - }, - options: [ - { - label: "PM2", - value: "pm2", - }, - { - label: "Manual", - value: "manual", - }, - { - label: "Docker", - value: "docker", - }, - { - label: "Cron", - value: "cron", - }, - ], - }, - ], - }, }); const buildConfigurationFile = () => { @@ -263,80 +260,92 @@ const Generator = ({ setConfiguration }) => { Create your configuration in a few clicks! - {Object.entries(data).map(([categorySlug, category]) => ( - - - - {category.name} - - {typeof category.enabled === "boolean" ? ( - - onCategoryToggleChange(categorySlug, enabled) - } - /> + {Object.entries(data).map( + ([categorySlug, category], index, entries) => ( + + + + {category.name} + + {typeof category.enabled === "boolean" ? ( + + onCategoryToggleChange(categorySlug, enabled) + } + /> + ) : null} + + + {category.description ? ( + Create your configuration in a few clicks! ) : null} - - {category.description ? ( - Create your configuration in a few clicks! - ) : null} + {category.warning ? ( + + + + + + + + + ) : null} - {category.warning ? ( - - - - - - - - - ) : null} + {category.enabled !== false + ? category.fields.map((field) => ( +
+ {field.warning ? ( + + + + + + + + + ) : null} - {category.enabled !== false - ? category.fields.map((field) => { - if (field.type === "boolean") { - return ( -
+ {field.type === "boolean" ? ( -
- ); - } - if (field.type === "string") { - return ( -
+ ) : null} + + {field.type === "string" || field.type === "number" ? ( -
- ); - } - if (field.type === "select") { - return ( -
+ ) : null} + + {field.type === "select" ? ( -
- ); - } - }) - : null} + ) : null} +
+ )) + : null} - -
- ))} + {index !== entries.length - 1 ? ( + + ) : null} +
+ ), + )}
); diff --git a/docs/src/components/platforms/index.js b/docs/src/components/platforms/index.js index 7afcdb3..bf8c329 100644 --- a/docs/src/components/platforms/index.js +++ b/docs/src/components/platforms/index.js @@ -1,58 +1,67 @@ -import Link from '@docusaurus/Link'; -import clsx from 'clsx'; -import PropTypes from 'prop-types'; -import React from 'react'; +import Link from "@docusaurus/Link"; +import clsx from "clsx"; +import PropTypes from "prop-types"; +import React from "react"; -import styles from './styles.module.css'; +import styles from "./styles.module.css"; const PlatformList = [ - { - name: 'Docker', - icon: '🐳', - }, - { - name: 'Cron', - icon: '⏰', - }, - { - name: 'PM2', - icon: '⏲️', - }, - { - name: 'Manual sync', - icon: '👏️', - }, + { + name: "Docker", + icon: "🐳", + }, + { + name: "Cron", + icon: "⏰", + }, + { + name: "PM2", + icon: "⏲️", + }, + { + name: "Manual sync", + icon: "👏️", + }, ]; Platform.propTypes = { - icon: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, }; PlatformList.propTypes = Array.from(Platform.propTypes); function Platform({ icon, name }) { - const page = name.toLowerCase().replaceAll(' ', '-'); - return ( - -
- {icon} -

Configure with
{name}

-
- - ); + const page = name.toLowerCase().replaceAll(" ", "-"); + return ( + +
+ + {icon} + +

+ Configure with +
+ {name} +

+
+ + ); } export default function Index() { - return ( -
-
-
- {PlatformList.map((props) => ( - - ))} -
-
-
- ); + return ( +
+
+
+ {PlatformList.map((props) => ( + + ))} +
+
+
+ ); } diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 0282a4b..15a95c7 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -30,3 +30,7 @@ --ifm-color-primary-lightest: #4fbcdd; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } + +table { + width:100%; +} diff --git a/package.json b/package.json index 83de8e7..f461904 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "test": "jest --config ./meta/testing/jest.config.cjs", "build": "tsc", "predeploy": "npm ci && npm run build", - "deploy": "bash ./deployment/deploy.sh", - "deploy:update": "bash ./deployment/deploy.sh --update", + "pm2": "bash deployment/pm2.sh", + "pm2:update": "bash deployment/pm2.sh --update", "postinstall": "husky install .husky", "commitlint": "commitlint --edit" }, diff --git a/src/constants.ts b/src/constants.ts index ae2bd10..22b4cd4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,26 +16,38 @@ if (!envAvailable) { throw new Error("No suitable .env file found."); } +const trimTwitterHandle = (handle: string) => { + return handle.toLowerCase().trim().replaceAll("@", ""); +}; + dotenv.config({ path: envPath }); -export const TWITTER_HANDLE = process.env.TWITTER_HANDLE || ""; -export const TWITTER_USERNAME = process.env.TWITTER_USERNAME || ""; -export const TWITTER_PASSWORD = process.env.TWITTER_PASSWORD || ""; -export const MASTODON_INSTANCE = process.env.MASTODON_INSTANCE || ""; -export const MASTODON_ACCESS_TOKEN = process.env.MASTODON_ACCESS_TOKEN || ""; -export const BLUESKY_INSTANCE = process.env.BLUESKY_INSTANCE || ""; -export const BLUESKY_IDENTIFIER = process.env.BLUESKY_IDENTIFIER || ""; -export const BLUESKY_PASSWORD = process.env.BLUESKY_PASSWORD || ""; -export const INSTANCE_ID = process.env.INSTANCE_ID ?? "instance"; -export const STORAGE_DIR = process.env.STORAGE_DIR ?? process.cwd(); -export const CACHE_PATH = `${STORAGE_DIR}/cache.${INSTANCE_ID.toLowerCase() +export const TWITTER_HANDLE = trimTwitterHandle( + process.env.TWITTER_HANDLE || "", +); +export const TWITTER_USERNAME = trimTwitterHandle( + process.env.TWITTER_USERNAME || "", +); +export const TWITTER_PASSWORD = (process.env.TWITTER_PASSWORD || "").trim(); +export const MASTODON_INSTANCE = (process.env.MASTODON_INSTANCE || "").trim(); +export const MASTODON_ACCESS_TOKEN = ( + process.env.MASTODON_ACCESS_TOKEN || "" +).trim(); +export const BLUESKY_INSTANCE = (process.env.BLUESKY_INSTANCE || "").trim(); +export const BLUESKY_IDENTIFIER = (process.env.BLUESKY_IDENTIFIER || "").trim(); +export const BLUESKY_PASSWORD = (process.env.BLUESKY_PASSWORD || "").trim(); +export const INSTANCE_ID = (TWITTER_HANDLE ?? "instance") + .toLowerCase() .trim() - .replaceAll(" ", "_")}.json`; -export const COOKIES_PATH = `${STORAGE_DIR}/cookies.${INSTANCE_ID.toLowerCase() - .trim() - .replaceAll(" ", "_")}.json`; + .replaceAll(" ", "_"); +export const STORAGE_DIR = process.env.STORAGE_DIR ?? process.cwd(); +export const CACHE_PATH = `${STORAGE_DIR}/cache.${INSTANCE_ID}.json`; +export const COOKIES_PATH = `${STORAGE_DIR}/cookies.${INSTANCE_ID}.json`; export const SYNC_MASTODON = (process.env.SYNC_MASTODON || "false") === "true"; export const SYNC_BLUESKY = (process.env.SYNC_BLUESKY || "false") === "true"; +export const SYNC_FREQUENCY_MIN = parseInt( + process.env.SYNC_FREQUENCY_MIN || "30", +); export const SYNC_PROFILE_DESCRIPTION = (process.env.SYNC_PROFILE_DESCRIPTION || "false") === "true"; export const SYNC_PROFILE_PICTURE = @@ -46,6 +58,5 @@ export const SYNC_PROFILE_HEADER = (process.env.SYNC_PROFILE_HEADER || "false") === "true"; export const DEBUG = (process.env.TOUITOMAMOUT_DEBUG || "false") === "true"; export const DAEMON = (process.env.DAEMON || "false") === "true"; -export const DAEMON_PERIOD_MIN = parseInt(process.env.DAEMON_PERIOD_MIN ?? "7"); // Default 7 min export const VOID = "∅"; export const API_RATE_LIMIT = parseInt(process.env.API_RATE_LIMIT ?? "10"); diff --git a/src/index.ts b/src/index.ts index 43b7399..e5d5e3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ import { configuration } from "./configuration/configuration.js"; import { DAEMON, - DAEMON_PERIOD_MIN, SYNC_BLUESKY, + SYNC_FREQUENCY_MIN, SYNC_MASTODON, TWITTER_HANDLE, } from "./constants.js"; @@ -64,11 +64,11 @@ const touitomamout = async () => { await touitomamout(); if (DAEMON) { - console.log(`Run daemon every ${DAEMON_PERIOD_MIN}min`); + console.log(`Run daemon every ${SYNC_FREQUENCY_MIN}min`); setInterval( async () => { await touitomamout(); }, - DAEMON_PERIOD_MIN * 60 * 1000, + SYNC_FREQUENCY_MIN * 60 * 1000, ); }