Compare commits

...

6 Commits

Author SHA1 Message Date
Elizabeth Cray
8c470f1362 Adjust ToDo 2025-02-06 22:28:27 -05:00
Elizabeth Cray
d9201697cf Merge branch 'main' of git.corrupt.link:liz/m2b 2025-02-06 22:20:06 -05:00
Elizabeth Cray
17f06d4a60 Create embeded link if no media 2025-02-06 22:20:03 -05:00
338f7fe79c Link issue 2025-02-01 04:42:42 +00:00
c3a481b592 Add todo 2025-02-01 04:40:38 +00:00
Elizabeth Cray
adb08cbdf3 Users are friends so I'll tell them things 2025-01-31 23:38:20 -05:00
4 changed files with 39 additions and 13 deletions

View File

@@ -1 +1,20 @@
# M2B - Mastodon ➡️ Bluesky crossposter
# M2B - Mastodon ➡️ Bluesky crossposter
FYI: This is a work-in-progress and I intend to clean up the code and make this *MUCH* more user-friendly.
## Why did I make this?
I was having a hard time finding a mastodon --> bluesky crossposting tool that supported replies and boosts on top of normal posts. This ended up wasting enough time to where I figured I'd just make my own.
## How to use
Don't forget to install dependencies with `npm i` and adjust the config by copying `example.env` into `.env`.
Get the mastodon credentials from going to user preferences > development > and creating a new application.
For bluesky credentials you have to make a new app password under settings > privacy and security > app passwords.
This should ideally be run from a cron job (specifically `run.sh`), but can be run with just `node index.js`.
## ToDo List
* Cleanup code
* Maybe fix the "reply to" add-in
* Maybe generate image preview of mastodon post

View File

@@ -1,6 +1,6 @@
MASTODON_TOKEN: ""
MASTODON_INSTANCE: "https://mastodon.social"
MASTODON_USER: ""
MASTODON_USER: "alice"
BSKY_INSTANCE: "https://bsky.social"
BSKY_USER: "username.bsky.social"
BSKY_USER: "alice.bsky.social"
BSKY_APP_PASS: ""

View File

@@ -88,16 +88,12 @@ if (existsSync(historyFile)){
history += readFileSync(historyFile, 'UTF-8').split('\n');
}
const bskyPost = async (text, media = []) => {
const bskyPost = async (text, postUrl, media = []) => {
let uploadedMedia = [];
for (let m of media){
// let mime = `${m.url}`.split('.').pop();
// mime = mime.toLowerCase();
// mime = mime === 'jpg'?'jpeg':mime;
const fileBuffer = Buffer.from(readFileSync(m.data));
const { ext, mimeT } = await fileTypeFromBuffer(fileBuffer);
let uploadResult = await bsky.uploadBlob(fileBuffer, {
// encoding: `image/${mime}`
encoding: mimeT
});
if (uploadResult.success){
@@ -124,6 +120,16 @@ const bskyPost = async (text, media = []) => {
"$type": "app.bsky.embed.images",
images: uploadedMedia
}
} else {
// set embed to mastodon post
post.embed = {
"$type": "app.bsky.embed.external",
external: {
uri: postUrl,
title: `Post from @${process.env.MASTODON_USER}@${process.env.MASTODON_INSTANCE.substring(8)}`,
description: text
}
}
}
console.log(JSON.stringify(post));
let bskyPostData = await bsky.post(post);
@@ -165,6 +171,7 @@ client.get(`/accounts/${process.env.MASTODON_ID}/statuses`, {
});
if (status.mentions > 0 && status.in_reply_to_id !== null){
// Post is a reply
// TODO: This isn't working yet
const replyTo = [...status.mentions].filter((mention) => mention.id === status.in_reply_to_account_id);
if (replyTo.length > 0){
text = `Reply to: ${replyTo[0].url}/@${replyTo[0].acct}/${status.in_reply_to_id}\n${text}`;
@@ -186,7 +193,7 @@ client.get(`/accounts/${process.env.MASTODON_ID}/statuses`, {
});
}
}
bskyPost(text, medias);
bskyPost(text, status.url, medias);
appendFileSync(historyFile, `\n${status.id}`);
} else {
console.log(`ERROR: ${status.url} is too long and unable to be reposted`);
@@ -194,9 +201,9 @@ client.get(`/accounts/${process.env.MASTODON_ID}/statuses`, {
}else{
// is boosted post
let text = status.reblog.url;
bskyPost(text, []);
bskyPost(text, status.url, []);
appendFileSync(historyFile, `\n${status.id}`);
}
}
}
});
});

4
run.sh Normal file → Executable file
View File

@@ -1,3 +1,3 @@
#!/bin/bash
/usr/bin/node index.mjs
rm .temp_*
/usr/bin/node /home/liz/m2b/index.mjs
rm .temp_*