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); } } 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 } }) } async function postToBluesky(text, images) { await agent.login({ identifier: process.env.BLUESKY_HANDLE, password: process.env.BLUESKY_PASSWORD, }); // 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.length > 0) { completetext = text + " " + images.map(img => img.url).join(" ") } const richText = new RichText({ text: completetext }); await richText.detectFacets(agent); return agent.post({ text: richText.text, facets: richText.facets, }); } function removeHtmlTags(input) { return input.replace(/<[^>]*>/g, ""); } 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}"); } // 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 || isReplyToMyself(item)) .reverse(); let newTimestampId = 0; for (item of reversed) { const currentTimestampId = Date.parse(item.published); if (currentTimestampId > newTimestampId) { newTimestampId = currentTimestampId; } if (currentTimestampId > lastProcessedPostId && lastProcessedPostId != 0) { const text = removeHtmlTags(convertQuote(item.object.content)); const images = await getImages(item); let {uri} = await postToBluesky(text, images); console.log(`posted ${item.object.id} to ${uri} . ${newTimestampId}`); } } if (newTimestampId > 0) { lastProcessedPostId = newTimestampId; saveLastProcessedPostId(); } } fetchNewPosts(); // Fetch new posts every 5 minutes (adjust as needed) setInterval(fetchNewPosts, 1 * 60 * 1000);