Compare commits

...

13 commits
1.0.0 ... main

Author SHA1 Message Date
Alex Hyett
744767a9e2 Fixed quotes and links 2024-11-08 14:32:27 +00:00
Alex Hyett
ab01098653 Upgraded packages to latest 2024-11-08 13:51:05 +00:00
Alex Hyett
94f3caf2fc Fix docker-compose
Some checks failed
CI / Build docker image (push) Has been cancelled
CI / Release (push) Has been cancelled
2024-11-03 13:29:30 +00:00
Maurice Renck
598b60d98a
chore: updated readme 2024-06-25 13:17:56 +02:00
semantic-release-bot
9057cb4a71 chore(release): 1.2.0 [skip ci]
# [1.2.0](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.1.1...v1.2.0) (2024-05-29)

### Features

* split long posts into replies ([#10](https://github.com/mauricerenck/mastodon-to-bluesky/issues/10)) ([b093c7d](b093c7d5fc))
2024-05-29 07:59:02 +00:00
René Schimmelpfennig
b093c7d5fc
feat: split long posts into replies (#10)
* post split text as reply

---------

Co-authored-by: Maurice Renck <maurice@maurice-renck.de>
2024-05-29 09:57:57 +02:00
semantic-release-bot
598fb3a7a9 chore(release): 1.1.1 [skip ci]
## [1.1.1](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.1.0...v1.1.1) (2024-04-24)

### Bug Fixes

* updating newTimestampId correctly ([0b753d2](0b753d234c))
2024-04-24 10:45:27 +00:00
René Schimmelpfennig
0b753d234c
fix: updating newTimestampId correctly
last PR from me introduce an error with the setting newTimestampId, so that's impossible to get new posts :/
2024-04-24 12:44:14 +02:00
semantic-release-bot
887ba8fcc7 chore(release): 1.1.0 [skip ci]
# [1.1.0](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.0.0...v1.1.0) (2024-04-03)

### Features

* add error-handling for Bluesky posting; truncate messages with more than 300 chars ([#7](https://github.com/mauricerenck/mastodon-to-bluesky/issues/7)) ([032677e](032677e371))
2024-04-03 08:03:23 +00:00
René Schimmelpfennig
032677e371
feat: add error-handling for Bluesky posting; truncate messages with more than 300 chars (#7) 2024-04-03 10:01:54 +02:00
semantic-release-bot
710c8b5089 chore(release): 1.0.0 [skip ci]
# 1.0.0 (2024-03-30)

### Bug Fixes

* job handling ([9f6e614](9f6e614b57))
* missing github env ([95d0406](95d0406ed3))
* token ([5bdf84a](5bdf84ae6d))

### Features

* INIT ([6d2887f](6d2887f667))
* semantic release ([9c87751](9c877515c5))
2024-03-30 16:09:49 +00:00
Maurice Renck
5bdf84ae6d
fix: token 2024-03-30 17:07:30 +01:00
Maurice Renck
b507721131
chore: test without token 2024-03-30 17:03:46 +01:00
9 changed files with 9292 additions and 9261 deletions

View file

@ -2,4 +2,5 @@ MASTODON_INSTANCE="https://mastodon.instance"
MASTODON_USER="username"
BLUESKY_ENDPOINT="https://bsky.social"
BLUESKY_HANDLE="USERNAME.bsky.social"
BLUESKY_PASSWORD="PASSWORD"
BLUESKY_PASSWORD="PASSWORD"
INTERVAL_MINUTES=5

View file

@ -1,83 +0,0 @@
name: CI
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
name: Build docker image
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
packages: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Read .nvmrc
run: echo "NODE_VERSION=$(awk -F. '{print $1}' .nvmrc)" >> $GITHUB_OUTPUT
id: nvm
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=schedule
type=ref,event=pr
type=semver,pattern={{version}}
type=sha,prefix={{branch}}-,format=short
type=sha,prefix=,format=short
{{branch}}
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
labels: ${{ steps.meta.outputs.labels }}
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
NODE_VERSION=${{ steps.nvm.outputs.NODE_VERSION }}
release:
name: Release
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read .nvmrc
run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT
id: nvm
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
- name: Install
run: npm ci --ignore-scripts
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.PAT_SEMANTIC_RELEASE }}
run: npx semantic-release

View file

@ -1,44 +0,0 @@
name: Release
on:
release:
types:
- created
env:
IMAGE_NAME: ${{ github.repository }}
REGISTRY: ghcr.io
jobs:
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
packages: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version
id: version
uses: battila7/get-version-action@v2
- name: Add Semantic Version tag to Docker Image
uses: shrink/actions-docker-registry-tag@v4
with:
registry: ${{ env.REGISTRY }}
repository: ${{ env.IMAGE_NAME }}
target: 'main'
tags: |
${{ steps.version.outputs.version-without-v }}
${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}
latest

35
CHANGELOG.md Normal file
View file

@ -0,0 +1,35 @@
# [1.2.0](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.1.1...v1.2.0) (2024-05-29)
### Features
* split long posts into replies ([#10](https://github.com/mauricerenck/mastodon-to-bluesky/issues/10)) ([b093c7d](https://github.com/mauricerenck/mastodon-to-bluesky/commit/b093c7d5fc6383da19c6434f57037e014da822de))
## [1.1.1](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.1.0...v1.1.1) (2024-04-24)
### Bug Fixes
* updating newTimestampId correctly ([0b753d2](https://github.com/mauricerenck/mastodon-to-bluesky/commit/0b753d234caff870e7a5fcb5b4bc4fac1a2e1acf))
# [1.1.0](https://github.com/mauricerenck/mastodon-to-bluesky/compare/v1.0.0...v1.1.0) (2024-04-03)
### Features
* add error-handling for Bluesky posting; truncate messages with more than 300 chars ([#7](https://github.com/mauricerenck/mastodon-to-bluesky/issues/7)) ([032677e](https://github.com/mauricerenck/mastodon-to-bluesky/commit/032677e3715566db9d26dd79a93d38776ddd4bc1))
# 1.0.0 (2024-03-30)
### Bug Fixes
* job handling ([9f6e614](https://github.com/mauricerenck/mastodon-to-bluesky/commit/9f6e614b57480c61c9ea6ccee76d43d354f6b700))
* missing github env ([95d0406](https://github.com/mauricerenck/mastodon-to-bluesky/commit/95d0406ed37f093282b2b6b53d17f5123702625d))
* token ([5bdf84a](https://github.com/mauricerenck/mastodon-to-bluesky/commit/5bdf84ae6d92a5a8034ca43daf8536fdaa7e15ec))
### Features
* INIT ([6d2887f](https://github.com/mauricerenck/mastodon-to-bluesky/commit/6d2887f667220d2a5914edc6179b297c2cd2602f))
* semantic release ([9c87751](https://github.com/mauricerenck/mastodon-to-bluesky/commit/9c877515c55555370360bbf9985c3cb1910ae11f))

View file

@ -1,3 +1,56 @@
# mastodon-to-bluesky
A Node.js script for crossposting from mastodon to bluesky
For more details see: https://maurice-renck.de/hub/tooling/crosspost-from-mastodon-to-bluesky
# Mastodon to Bluesky
This is forked from https://github.com/mauricerenck/mastodon-to-bluesky. So far I have only added the following:
- Upgraded packages due to issues with conversion to Bluesky.
- Updated docker-compose to build the Docker image.
#### Crosspost from Mastodon to Bluesky
![GitHub release](https://img.shields.io/github/release/mauricerenck/mastodon-to-bluesky.svg?maxAge=1800) ![License](https://img.shields.io/github/license/mashape/apistatus.svg)
---
This scripts listens to your Mastodon account and crossposts your toots to your Bluesky account. It uses the Mastodon API and the Bluesky API to achieve this. The script is written in Node.js and can be run on your local machine or on a server.
---
## Installation
You can run the script directly using Node.js or you can use the Docker image.
### Node.js
Clone this repository and install the dependencies:
```bash
git clone https://code.alexhyett.com/alexhyett/mastodon-to-bluesky.git
cd mastodon-to-bluesky
npm install
```
## Configuration
Create a `.env` file in the root directory of the project and add the following variables:
```bash
MASTODON_INSTANCE: 'https://mastodon.instance'
MASTODON_USER: 'username'
BLUESKY_ENDPOINT: 'https://bsky.social'
BLUESKY_HANDLE: 'USERNAME.bsky.social'
BLUESKY_PASSWORD: 'PASSWORD'
INTERVAL_MINUTES: 5
```
You can also set the same variables as environment variables in the `docker-compose.yml` file.
## Usage
To run the script, execute the following command:
```bash
node main.js
```
---
For more details see: https://maurice-renck.de/hub/tooling/crosspost-from-mastodon-to-bluesky

View file

@ -1,19 +1,12 @@
version: '3'
services:
app:
image: host/mastodon-to-bluesky:latest
build: .
container_name: mastodon-to-bluesky
environment:
MASTODON_INSTANCE: 'https://mastodon.instance'
MASTODON_USER: 'username'
BLUESKY_ENDPOINT: 'https://bsky.social'
BLUESKY_HANDLE: 'USERNAME.bsky.social'
BLUESKY_PASSWORD: 'PASSWORD'
INTERVAL_MINUTES: 5
env_file: '.env'
volumes:
- mastodon-to-bluesky:/usr/src/app/data
restart: unless-stopped
volumes:
mastodon-to-bluesky:
external: true
mastodon-to-bluesky:

209
main.js
View file

@ -1,95 +1,154 @@
require("dotenv").config();
const fs = require("fs");
const path = require("path");
const { RichText, BskyAgent } = require("@atproto/api");
const { RichText, AtpAgent } = require("@atproto/api");
const axios = require("axios");
const he = require('he');
// Mastodon credentials
const mastodonInstance = process.env.MASTODON_INSTANCE;
const mastodonUser = process.env.MASTODON_USER;
// Bluesky agent
const agent = new BskyAgent({ service: process.env.BLUESKY_ENDPOINT });
// File to store the last processed Mastodon post ID
const lastProcessedPostIdFile = path.join(
__dirname,
"data",
"lastProcessedPostId.txt"
);
// Variable to store the last processed Mastodon post ID
let lastProcessedPostId = loadLastProcessedPostId();
// Function to load the last processed post ID from the file
function loadLastProcessedPostId() {
try {
return fs.readFileSync(lastProcessedPostIdFile, "utf8").trim();
} catch (error) {
console.error("Error loading last processed post ID:", error);
return null;
}
}
// Function to save the last processed post ID to the file
function saveLastProcessedPostId() {
try {
fs.writeFileSync(lastProcessedPostIdFile, `${lastProcessedPostId}`);
} catch (error) {
console.error("Error saving last processed post ID:", error);
}
}
async function postToBluesky(text) {
await agent.login({
async function main() {
// Bluesky agent
const agent = new AtpAgent({ service: process.env.BLUESKY_ENDPOINT });
const loginResponse = await agent.login({
identifier: process.env.BLUESKY_HANDLE,
password: process.env.BLUESKY_PASSWORD,
});
if (!loginResponse.success) console.error("🔒 login failed");
const richText = new RichText({ text });
await richText.detectFacets(agent);
await agent.post({
text: richText.text,
facets: richText.facets,
});
}
function removeHtmlTags(input) {
return input.replace(/<[^>]*>/g, "");
}
// Function to periodically fetch new Mastodon posts
async function fetchNewPosts() {
const response = await axios.get(
`${mastodonInstance}/users/${mastodonUser}/outbox?page=true`
// File to store the last processed Mastodon post ID
const lastProcessedPostIdFile = path.join(
__dirname,
"data",
"lastProcessedPostId.txt"
);
const reversed = response.data.orderedItems
.filter((item) => item.object.type === "Note")
.filter((item) => item.object.inReplyTo === null)
.reverse();
// Variable to store the last processed Mastodon post ID
let lastProcessedPostId = loadLastProcessedPostId();
let newTimestampId = 0;
reversed.forEach((item) => {
const currentTimestampId = Date.parse(item.published);
if (currentTimestampId > newTimestampId) {
newTimestampId = currentTimestampId;
// Function to load the last processed post ID from the file
function loadLastProcessedPostId() {
try {
return fs.readFileSync(lastProcessedPostIdFile, "utf8").trim();
} catch (error) {
console.error("Error loading last processed post ID:", error);
return null;
}
if (currentTimestampId > lastProcessedPostId && lastProcessedPostId != 0) {
const text = removeHtmlTags(item.object.content);
postToBluesky(text);
}
});
if (newTimestampId > 0) {
lastProcessedPostId = newTimestampId;
saveLastProcessedPostId();
}
// Function to save the last processed post ID to the file
function saveLastProcessedPostId() {
try {
fs.writeFileSync(lastProcessedPostIdFile, `${lastProcessedPostId}`);
} catch (error) {
console.error("Error saving last processed post ID:", error);
}
}
async function createBlueskyMessage(text) {
const richText = new RichText({ text });
await richText.detectFacets(agent);
return {
text: richText.text,
facets: richText.facets
};
}
async function postToBluesky(textParts) {
const blueskyMessage = await createBlueskyMessage(textParts[0]);
const rootMessageResponse = await agent.post(blueskyMessage);
if (textParts.length === 1) return;
let replyMessageResponse = null
for (let index = 1; index < textParts.length; index++) {
replyMessageResponse = await agent.post({
...(await createBlueskyMessage(textParts[index])),
reply: {
root: rootMessageResponse,
parent: replyMessageResponse ?? rootMessageResponse,
}
});
}
}
function sanitizeHtml(input) {
const withoutHtml = input.replace(/<[^>]*>/g, "");
const decodeQuotes = he.decode(withoutHtml);
const addSpace = decodeQuotes.replace(/(https?:\/\/)/g, ' $1');
return addSpace;
}
function splitText(text, maxLength) {
// Split the text by spaces
const words = text.split(" ");
let result = [];
let currentChunk = "";
for (const word of words) {
// Add the current word to the current chunk
const potentialChunk = `${currentChunk} ${word}`.trim();
if (potentialChunk.length <= maxLength) {
// If the current chunk is still under max length, add the word
currentChunk = potentialChunk;
} else {
// Otherwise, add the current chunk to the result and start a new chunk
result.push(currentChunk);
currentChunk = word;
}
}
// Add the last chunk to the result
result.push(currentChunk);
return result;
}
// Function to periodically fetch new Mastodon posts
async function fetchNewPosts() {
const response = await axios.get(
`${mastodonInstance}/users/${mastodonUser}/outbox?page=true`
);
const reversed = response.data.orderedItems
.filter((item) => item.object.type === "Note")
.filter((item) => item.object.inReplyTo === null)
.reverse();
let newTimestampId = 0;
reversed.forEach((item) => {
const currentTimestampId = Date.parse(item.published);
if (currentTimestampId > newTimestampId) {
newTimestampId = currentTimestampId;
}
if (currentTimestampId > lastProcessedPostId && lastProcessedPostId != 0) {
try {
console.log('📧 posting to BlueSky', currentTimestampId)
const textParts = splitText(sanitizeHtml(item.object.content), 300);
postToBluesky(textParts);
} catch (error) {
console.error('🔥 can\'t post to Bluesky', currentTimestampId, error)
}
}
});
if (newTimestampId > 0) {
lastProcessedPostId = newTimestampId;
saveLastProcessedPostId();
}
}
fetchNewPosts();
// Fetch new posts every 5 minutes (adjust as needed)
setInterval(fetchNewPosts, (process.env.INTERVAL_MINUTES ?? 5) * 60 * 1000);
}
fetchNewPosts();
// Fetch new posts every 5 minutes (adjust as needed)
setInterval(fetchNewPosts, (process.env.INTERVAL_MINUTES ?? 5) * 60 * 1000);
main()

18072
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,20 @@
{
"name": "mastodon-to-bluesky",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Maurice Renck <hello@maurice-renck.de>",
"license": "MIT",
"dependencies": {
"@atproto/api": "^0.12.2",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"axios": "^1.6.8",
"dotenv": "^16.4.5",
"mastodon-api": "^1.3.0"
}
"name": "mastodon-to-bluesky",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Maurice Renck <hello@maurice-renck.de>",
"license": "MIT",
"dependencies": {
"@atproto/api": "^0.13.15",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"axios": "^1.7.7",
"dotenv": "^16.4.5",
"he": "^1.2.0",
"mastodon-api": "^1.3.0"
}
}