2024-01-12 12:30:45 +00:00
|
|
|
require("dotenv").config();
|
|
|
|
const fs = require("fs");
|
|
|
|
const path = require("path");
|
|
|
|
const { RichText, BskyAgent } = require("@atproto/api");
|
|
|
|
const axios = require("axios");
|
|
|
|
|
|
|
|
// 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, "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);
|
|
|
|
}
|
|
|
|
}
|
2024-03-04 03:27:13 +00:00
|
|
|
async function getImages(item) {
|
|
|
|
const responses = item.object.attachment
|
|
|
|
.filter(attachment => attachment.mediaType.includes("image"));
|
|
|
|
const infos = responses.map(attachment => {
|
|
|
|
return {
|
|
|
|
url: attachment.url,
|
|
|
|
width: attachment.width,
|
|
|
|
height: attachment.height
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const respromise = await Promise.all(responses.map(attachment =>
|
|
|
|
axios.get(
|
|
|
|
attachment.url,
|
|
|
|
{
|
|
|
|
responseType: "arraybuffer"
|
|
|
|
}
|
|
|
|
)
|
|
|
|
));
|
|
|
|
|
|
|
|
return respromise.map((buf, index) => {
|
|
|
|
return {
|
|
|
|
blob: new Blob(
|
|
|
|
[buf.data],
|
|
|
|
{
|
|
|
|
type: buf.headers["content-type"]
|
|
|
|
}
|
|
|
|
),
|
|
|
|
url: infos[index].url,
|
|
|
|
width: infos[index].width,
|
|
|
|
height: infos[index].height
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-01-12 12:30:45 +00:00
|
|
|
|
2024-03-04 03:27:13 +00:00
|
|
|
async function postToBluesky(text, images) {
|
2024-01-12 12:30:45 +00:00
|
|
|
await agent.login({
|
|
|
|
identifier: process.env.BLUESKY_HANDLE,
|
|
|
|
password: process.env.BLUESKY_PASSWORD,
|
|
|
|
});
|
|
|
|
|
2024-03-04 03:27:13 +00:00
|
|
|
// let imgdatas = await Promise.all(images.map(async img => {
|
|
|
|
// const buf = await img.blob.arrayBuffer();
|
|
|
|
// console.log(`detected img: ${buf}, ${buf}`);
|
|
|
|
// const dataArray = new Uint8Array(buf);
|
|
|
|
// const data = await agent.uploadBlob(
|
|
|
|
// dataArray,
|
|
|
|
// {
|
|
|
|
// // 画像の形式を指定 ('image/jpeg' 等の MIME タイプ)
|
|
|
|
// encoding: img.type,
|
|
|
|
// }
|
|
|
|
// );
|
|
|
|
// return {
|
|
|
|
// alt: "",
|
|
|
|
// image: data.data.blob,
|
|
|
|
// aspectRatio: {
|
|
|
|
// width: img.width,
|
|
|
|
// height: img.height
|
|
|
|
// }
|
|
|
|
// };
|
|
|
|
// }));
|
|
|
|
let completetext = text;
|
|
|
|
if (!images.empty()) {
|
|
|
|
completetext = text + " " + images.map(img => img.url).join(" ")
|
|
|
|
}
|
|
|
|
const richText = new RichText({ completetext });
|
2024-01-12 12:30:45 +00:00
|
|
|
await richText.detectFacets(agent);
|
2024-03-04 03:27:13 +00:00
|
|
|
|
2024-01-12 12:30:45 +00:00
|
|
|
await agent.post({
|
|
|
|
text: richText.text,
|
|
|
|
facets: richText.facets,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeHtmlTags(input) {
|
|
|
|
return input.replace(/<[^>]*>/g, "");
|
|
|
|
}
|
2024-03-04 03:27:13 +00:00
|
|
|
function convertQuote(input) {
|
|
|
|
return input.replace(/"/g, '"');
|
|
|
|
}
|
|
|
|
function isReplyToMyself(item) {
|
|
|
|
const uri = item.object.inReplyTo;
|
|
|
|
if (uri === null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return uri.includes("${mastodonInstance}/users/${mastodonUser}");
|
|
|
|
}
|
2024-01-12 12:30:45 +00:00
|
|
|
// 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')
|
2024-03-04 03:27:13 +00:00
|
|
|
.filter(item => item.object.inReplyTo === null || isReplyToMyself(item))
|
|
|
|
.reverse();
|
2024-01-12 12:30:45 +00:00
|
|
|
|
|
|
|
let newTimestampId = 0;
|
2024-03-04 03:27:13 +00:00
|
|
|
|
|
|
|
await Promise.all(reversed.map(async item => {
|
2024-01-12 12:30:45 +00:00
|
|
|
const currentTimestampId = Date.parse(item.published);
|
|
|
|
|
2024-03-04 03:27:13 +00:00
|
|
|
if (currentTimestampId > newTimestampId) {
|
2024-01-12 12:30:45 +00:00
|
|
|
newTimestampId = currentTimestampId;
|
|
|
|
}
|
|
|
|
|
2024-03-04 03:27:13 +00:00
|
|
|
if (currentTimestampId > lastProcessedPostId && lastProcessedPostId != 0) {
|
|
|
|
const text = removeHtmlTags(convertQuote(item.object.content));
|
|
|
|
const images = await getImages(item);
|
|
|
|
postToBluesky(text, images);
|
|
|
|
console.log(`posted ${item.object.id}. ${newTimestampId}`);
|
2024-01-12 12:30:45 +00:00
|
|
|
}
|
2024-03-04 03:27:13 +00:00
|
|
|
}))
|
2024-01-12 12:30:45 +00:00
|
|
|
|
2024-03-04 03:27:13 +00:00
|
|
|
if (newTimestampId > 0) {
|
2024-01-12 12:30:45 +00:00
|
|
|
lastProcessedPostId = newTimestampId;
|
|
|
|
saveLastProcessedPostId();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fetchNewPosts();
|
|
|
|
// Fetch new posts every 5 minutes (adjust as needed)
|
2024-03-04 03:27:13 +00:00
|
|
|
setInterval(fetchNewPosts, 1 * 60 * 1000);
|