import "dotenv/config"; import {Bot, RichText, Post, IncomingChatPreference} from "@skyware/bot"; import {DidResolver} from "@atproto/identity"; import {readFile,writeFile} from "fs/promises"; var didres = new DidResolver({}); var getServiceEndpoint = async did => (await didres.resolve(did)).service.find(s => s.id == "#atproto_pds")?.serviceEndpoint; var bot = new Bot({emitChatEvents: true}); bot.on("error", console.error); try { var sessionData = JSON.parse(await readFile("session.json", "utf8")); await bot.resumeSession(sessionData); console.log("resumed session"); } catch(error) { console.log(error.message); var sessionData = await bot.login({ identifier: process.env.IDENTIFIER, password: process.env.PASSWORD }); console.log("new session:", sessionData); await writeFile("session.json", JSON.stringify(sessionData)); } await bot.setChatPreference(IncomingChatPreference.All); bot.on("mention", async post => { try { var responsePayload = await processRequest(post); if (responsePayload) await post.reply(responsePayload); } catch (error) { console.error(error); await post.reply({text: String(error.message?.substring(0,300))}); } }); bot.on("message", async (message) => { try { console.log(`chat: ${JSON.stringify({id: message.id, senderDid: message.senderDid, sentAt: message.sentAt, text: message.text, facets: message.facets, embed: message.embed})}`); var conversationId = message.conversationId; if (!conversationId) { console.warn("missing conversationId"); return; } if (message.embed) { var uri = message.embed.uri; } else { var uri = findLinkedBlueskyPosts(message.facets)[0]; //todo support multiple if (!uri) { await bot.sendMessage({conversationId, text: "Send me a Bluesky post with a video to get a download link!"}); return; } } try { var post = await bot.getPost(uri); } catch (error) { console.error(error); await bot.sendMessage({conversationId, text: error.message}); return; } var responsePayload = await processRequest(post); if (!responsePayload) { responsePayload = {text: "This post does not have a video :("}; }; await bot.sendMessage({conversationId, ...responsePayload}); } catch (error) { console.error(error); } }); async function processRequest(post) { // video in mentioning post or quoted post var vid = await findVideoInPost(post); // ignore original posts without video if (!vid && !post.replyRef) return; // video in replied post or quoted in replied post if (!vid) { var parentPost = await bot.getPost(post.replyRef.parent.uri); vid = await findVideoInPost(parentPost); } // video in original post or quoted in original post if (!vid) { var rootPost = await bot.getPost(post.replyRef.root.uri); vid = await findVideoInPost(rootPost); } if (!vid) { return {text: "Could not find Bluesky video in replied post, quoted post, or parent post"}; } var url = `${await getServiceEndpoint(vid.did)}/xrpc/com.atproto.sync.getBlob?did=${vid.did}&cid=${vid.cid}`; // pdes don't set a suitable Content-Disposition header var url2 = `https://avps.owo69.me/download.html?filename=${vid.cid}.mp4&url=${encodeURIComponent(url)}`; var text = new RichText().addLink("Click or touch here to download this video.", url2).addText("\n\nOr right click ").addLink("here", url).addText(` to “Save Link As…”`); return {text}; } async function findVideoInPost(post, b) { if (!post.embed) return; if (post.embed.isVideo()) return {did: post.author.did, cid: post.embed.cid}; if (post.embed.isRecordWithMedia() && post.embed.media.isVideo()) return {did: post.author.did, cid: post.embed.media.cid}; if (post.embed.isRecord() && post.embed.record instanceof Post && !b) { // post.embed.record.embed is undefined let quotedPost = await bot.getPost(post.embed.record.uri); return await findVideoInPost(quotedPost, true); } } function findLinkedBlueskyPosts(facets) { var uris = []; if (facets) for (var facet of facets) if (facet.features) for (var feature of facet.features) if (feature.$type == "app.bsky.richtext.facet#link" && feature.uri) { var match = feature.uri.match(/https?:\/\/bsky\.app\/profile\/([^\/]+)\/post\/([^\/]+)/); if (match) uris.push(`at://${match[1]}/app.bsky.feed.post/${match[2]}`); } return uris; }