CLI tool to sync X (Twitter) Bookmarks to Raindrop.io
Project description
x2raindrop-cli
A Python CLI tool to sync your X (Twitter) bookmarks to Raindrop.io collections.
Features
- Sync X bookmarks to a specified Raindrop.io collection
- Configurable link handling:
- Use X post permalink
- Use first external URL from the post (with fallback to permalink)
- Both: create entries for external URLs with X permalink stored in notes
- Apply custom tags to synced bookmarks
- Optional: Remove bookmarks from X after syncing
- Idempotent syncing with local state tracking
- Dry-run mode for safe testing
- Interactive OAuth 2.0 PKCE authentication flow for X
Requirements
- Python 3.12 or higher
- uv for dependency and environment management
- X Developer account with OAuth 2.0 app
- Raindrop.io account with API token
Installation
From PyPI (Recommended)
pip install x2raindrop-cli
From Source
git clone https://github.com/dotWee/x2raindrop-cli.git
cd x2raindrop-cli
# Install dependencies
uv sync
Using Docker
Pull the image from GitHub Container Registry:
docker pull ghcr.io/dotwee/x2raindrop-cli:latest
Run commands by mounting your local directory (for config and state persistence):
# Show help
docker run --rm ghcr.io/dotwee/x2raindrop-cli --help
# Initialize config in current directory
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli config init
# Sync bookmarks
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli sync --collection 12345
See the Docker Usage section for more details.
2. Set Up X API Credentials
You have two options for X authentication:
Option A: Direct Access Token (Simplest)
If you already have an access token (e.g., from another OAuth flow or the X Developer Portal):
- Set
X_ACCESS_TOKENin your config or environment - No browser login required - just run sync directly
[x]
access_token = "your_access_token_here"
Option B: OAuth 2.0 PKCE Flow (Interactive)
For browser-based login:
- Go to the X Developer Portal
- Create a new project and app (or use an existing one)
- Under "User authentication settings", configure:
- App permissions: Read and write
- Type of App: Native App (for PKCE without client secret) or Confidential Client
- Callback URL:
http://127.0.0.1:8765/callback
- Note your Client ID (and Client Secret if using Confidential Client)
- Run
x2raindrop x loginto authenticate
Required OAuth 2.0 Scopes:
bookmark.read- Read your bookmarksbookmark.write- Remove bookmarks (optional, only if using--remove-from-x)tweet.read- Read tweet datausers.read- Read user profile dataoffline.access- Refresh tokens for persistent access
3. Set Up Raindrop.io API Token
- Go to Raindrop.io Integrations
- Under "For Developers", create a new app or use "Test token"
- Copy the Test token for personal use
4. Configure the Application
Create a configuration file:
# Create default config file in current directory
uv run x2raindrop config init
# Edit the config file
nano config.toml
Or use environment variables:
# X API credentials (choose one method)
# Option A: Direct access token
export X_ACCESS_TOKEN="your_access_token"
# Option B: OAuth PKCE flow (then run `x2raindrop x login`)
export X_CLIENT_ID="your_client_id"
export X_CLIENT_SECRET="your_client_secret" # Optional for public clients
# Raindrop.io credentials
export RAINDROP_TOKEN="your_raindrop_token"
# Sync settings
export SYNC_COLLECTION_ID="12345" # Target collection ID
export SYNC_TAGS='["x-bookmark", "auto-synced"]' # JSON array format
export SYNC_REMOVE_FROM_X="false"
export SYNC_LINK_MODE="permalink" # permalink, first_external_url, or both
Usage
Authenticate with X
First, authenticate with X using the interactive OAuth 2.0 PKCE flow:
uv run x2raindrop x login
This will open your browser for authorization. After approving, the tokens are saved locally.
List Raindrop.io Collections
Find the collection ID you want to sync to:
uv run x2raindrop raindrop collections
Sync Bookmarks
Basic sync:
uv run x2raindrop sync --collection 12345
With options:
# Sync with custom tags
uv run x2raindrop sync --collection 12345 --tags "x,bookmarks,auto"
# Use first external URL from tweets
uv run x2raindrop sync --collection 12345 --link-mode first_external_url
# Remove from X after syncing (use with caution!)
uv run x2raindrop sync --collection 12345 --remove-from-x
# Dry run - see what would happen without making changes
uv run x2raindrop sync --collection 12345 --dry-run
Check X Authentication Status
uv run x2raindrop x status
Logout from X
uv run x2raindrop x logout
Configuration Reference
Config File Location
Default: config.toml in the current working directory (project root).
Override with --config flag on any command.
Config File Format
log_level = "INFO"
[x]
# Option A: Direct access token (simplest - no browser login needed)
access_token = ""
# Option B: OAuth PKCE flow (use `x2raindrop x login`)
client_id = ""
client_secret = "" # Leave empty for public clients
redirect_uri = "http://127.0.0.1:8765/callback"
scopes = [
"bookmark.read",
"bookmark.write",
"tweet.read",
"users.read",
"offline.access",
]
[raindrop]
token = "YOUR_RAINDROP_TOKEN"
[sync]
collection_id = 12345
collection_title = "" # Optional: look up collection by title
tags = ["x-bookmark", "auto-synced"]
remove_from_x = false
link_mode = "permalink" # permalink, first_external_url, or both
both_behavior = "one_external_plus_note" # one_external_plus_note or two_raindrops
dry_run = false
Link Modes
| Mode | Description |
|---|---|
permalink |
Create a Raindrop with the X post URL |
first_external_url |
Use the first external URL in the tweet (falls back to permalink if none) |
both |
Create entries for both external URL and permalink (behavior configurable) |
Both Behavior Options
When link_mode = "both" and the tweet contains an external URL:
| Option | Description |
|---|---|
one_external_plus_note |
Create one Raindrop for the external URL, store X permalink in the note |
two_raindrops |
Create two separate Raindrops (one for external URL, one for X permalink) |
Data Storage
The tool stores data in the current working directory:
config.toml- Configuration file.x2raindrop/x_token.json- X OAuth tokens (keep secure!).x2raindrop/state.json- Sync state for idempotency
Safety Notes
- Dry Run First: Always use
--dry-runbefore syncing to preview changes - Remove from X: The
--remove-from-xflag permanently removes bookmarks from X. Use with caution and consider backing up first - Token Security: The
x_token.jsonfile contains sensitive tokens. Ensure proper file permissions
X API Rate Limits
IMPORTANT: X API has strict rate limits, especially on the Free Tier.
| Tier | Rate Limit | Notes |
|---|---|---|
| Free | 1 request / 15 min | Very limited - sync may take a long time |
| Basic | Higher limits | Check X Developer Portal for current limits |
API Request Breakdown:
- Fetching bookmarks: 1 request per 100 bookmarks (paginated)
- Deleting a bookmark: 1 request per bookmark
Rate Limit Behavior: The CLI now uses the official Python XDK for X API calls. If X returns a 429 rate-limit response, the command exits with the API error from the SDK.
Recommendations for Free Tier:
- Don't use
--remove-from-x- each deletion is a separate request - Wait for the current rate-limit window to reset, then rerun the command
- The tool tracks synced bookmarks locally, so interrupted syncs can resume
- Consider upgrading to Basic tier if you have many bookmarks
Docker Usage
The Docker image provides a convenient way to run x2raindrop-cli without installing Python dependencies locally.
Pulling the Image
# Latest version
docker pull ghcr.io/dotwee/x2raindrop-cli:latest
# Specific version
docker pull ghcr.io/dotwee/x2raindrop-cli:1.0.3
Running Commands
The container's working directory is /data. Mount your local directory there to persist configuration and state:
# Create an alias for convenience
alias x2raindrop='docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli'
# Now use it like the native CLI
x2raindrop --version
x2raindrop config init
x2raindrop raindrop collections
x2raindrop sync --collection 12345 --dry-run
Using Environment Variables
Pass credentials via environment variables instead of a config file:
docker run --rm \
-e X_ACCESS_TOKEN="your_token" \
-e RAINDROP_TOKEN="your_raindrop_token" \
-e SYNC_COLLECTION_ID="12345" \
-v "$PWD":/data \
ghcr.io/dotwee/x2raindrop-cli sync
OAuth Authentication in Docker
The interactive OAuth 2.0 PKCE flow (x2raindrop x login) requires a browser, which doesn't work well inside a container. You have two options:
Option 1: Use a Direct Access Token (Recommended for Docker)
Set X_ACCESS_TOKEN in your config or as an environment variable. No browser login required.
Option 2: Authenticate on Host, Then Use in Docker
- Install the CLI locally and run
x2raindrop x loginon your host machine - This creates
.x2raindrop/x_token.jsonin your current directory - Mount that directory when running Docker:
docker run --rm -v "$PWD":/data ghcr.io/dotwee/x2raindrop-cli sync --collection 12345
The container will use the token file from your mounted directory.
Data Persistence
The container stores data in /data (the working directory):
| File | Purpose |
|---|---|
config.toml |
Configuration file |
.x2raindrop/x_token.json |
X OAuth tokens |
.x2raindrop/state.json |
Sync state for idempotency |
Always mount a volume to /data to persist this data between runs.
Development
Setup Development Environment
uv sync --group dev
Run Tests
uv run pytest
Run Tests with Coverage
uv run pytest --cov=x2raindrop_cli --cov-report=html
Linting
uv run ruff check src tests
uv run ruff format src tests
Type Checking
uv run ty check src
Troubleshooting
"Not authenticated with X"
Run x2raindrop x login to authenticate.
"Token expired"
The tool automatically refreshes tokens. If issues persist, run x2raindrop x logout then x2raindrop x login.
"Collection ID not found"
Run x2raindrop raindrop collections to list available collections and their IDs.
Rate Limit Errors
Wait 15 minutes and try again. X API allows 180 bookmark requests per 15-minute window.
License
Copyright (c) 2026 Lukas 'dotWee' Wolfsteiner lukas@wolfsteiner.media
Licensed under the Do What The Fuck You Want To Public License. See the LICENSE file for details.
Credits
- python-raindropio - Raindrop.io API wrapper
- X Python XDK - X API documentation
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file x2raindrop_cli-1.0.3.tar.gz.
File metadata
- Download URL: x2raindrop_cli-1.0.3.tar.gz
- Upload date:
- Size: 77.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf4e44e31525d7a2eb0b3f9f9b126b46f6116d796fb8b741b2ee7793a7e555ce
|
|
| MD5 |
ec7eb2e55aa4491ae68c6d4a5ad30c9c
|
|
| BLAKE2b-256 |
be699bba4bf2e717e92596bcd2a3f0c9e6950bc3c8a3ab00ee31a20ffa379d96
|
Provenance
The following attestation bundles were made for x2raindrop_cli-1.0.3.tar.gz:
Publisher:
release.yml on dotWee/py-x2raindrop-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
x2raindrop_cli-1.0.3.tar.gz -
Subject digest:
bf4e44e31525d7a2eb0b3f9f9b126b46f6116d796fb8b741b2ee7793a7e555ce - Sigstore transparency entry: 1349780928
- Sigstore integration time:
-
Permalink:
dotWee/py-x2raindrop-cli@22aebc754028364f89d2e6ee02d6ead2c9ee60b6 -
Branch / Tag:
refs/tags/v1.0.3 - Owner: https://github.com/dotWee
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22aebc754028364f89d2e6ee02d6ead2c9ee60b6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file x2raindrop_cli-1.0.3-py3-none-any.whl.
File metadata
- Download URL: x2raindrop_cli-1.0.3-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62911827f475a671d1d4a465f0267c6177d1f4f5acee1486a068430d57ad7174
|
|
| MD5 |
711f2b836262c6eeb07e37a70089f997
|
|
| BLAKE2b-256 |
bc4bedbe6b42a0e1167bc7e3ffdee2203803f11930a6feffd56c3b6df9edfced
|
Provenance
The following attestation bundles were made for x2raindrop_cli-1.0.3-py3-none-any.whl:
Publisher:
release.yml on dotWee/py-x2raindrop-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
x2raindrop_cli-1.0.3-py3-none-any.whl -
Subject digest:
62911827f475a671d1d4a465f0267c6177d1f4f5acee1486a068430d57ad7174 - Sigstore transparency entry: 1349780978
- Sigstore integration time:
-
Permalink:
dotWee/py-x2raindrop-cli@22aebc754028364f89d2e6ee02d6ead2c9ee60b6 -
Branch / Tag:
refs/tags/v1.0.3 - Owner: https://github.com/dotWee
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22aebc754028364f89d2e6ee02d6ead2c9ee60b6 -
Trigger Event:
push
-
Statement type: