Monorepo for Aesthetic.Computer aesthetic.computer
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 145 lines 4.3 kB view raw
1// news-atproto.mjs 2// Helper functions for syncing news to ATProto PDS 3// 2026.01.28 4 5import { AtpAgent } from "@atproto/api"; 6import { shell } from "./shell.mjs"; 7 8const PDS_URL = process.env.PDS_URL || "https://at.aesthetic.computer"; 9const NEWS_COLLECTION = "computer.aesthetic.news"; 10 11/** 12 * Create a news item on ATProto PDS using the submitting user's account 13 * @param {Object} database - MongoDB connection 14 * @param {string} sub - User sub (auth0|...) 15 * @param {Object} newsData - { headline, body?, link?, tags?, when } 16 * @param {string} refId - Database record _id as string 17 * @returns {Promise<{rkey: string, uri: string, did: string} | {error: string}>} 18 */ 19export async function createNewsOnAtproto(database, sub, newsData, refId) { 20 const users = database.db.collection("users"); 21 22 // Look up user's ATProto credentials 23 const user = await users.findOne({ _id: sub }); 24 25 if (!user?.atproto?.did || !user?.atproto?.password) { 26 shell.log(`ℹ️ User ${sub} has no ATProto account, skipping news sync`); 27 return { error: "No ATProto account" }; 28 } 29 30 const atprotoDid = user.atproto.did; 31 const atprotoPassword = user.atproto.password; 32 33 try { 34 const agent = new AtpAgent({ service: PDS_URL }); 35 await agent.login({ 36 identifier: atprotoDid, 37 password: atprotoPassword, 38 }); 39 40 const record = { 41 $type: NEWS_COLLECTION, 42 headline: newsData.headline, 43 when: newsData.when?.toISOString() || new Date().toISOString(), 44 ref: refId, 45 }; 46 47 // Add optional fields 48 if (newsData.body) record.body = newsData.body; 49 if (newsData.link) record.link = newsData.link; 50 if (newsData.tags?.length) record.tags = newsData.tags; 51 52 const result = await agent.com.atproto.repo.createRecord({ 53 repo: atprotoDid, 54 collection: NEWS_COLLECTION, 55 record, 56 }); 57 58 const uri = result.uri || result.data?.uri; 59 if (!uri) { 60 shell.error(`⚠️ ATProto response missing URI: ${JSON.stringify(result)}`); 61 return { error: "Missing URI in response" }; 62 } 63 64 const rkey = uri.split("/").pop(); 65 shell.log(`📰 Created ATProto news for ${atprotoDid}: ${rkey}`); 66 67 return { rkey, uri, did: atprotoDid }; 68 } catch (error) { 69 shell.error(`❌ Failed to create news on ATProto: ${error.message}`); 70 return { error: error.message }; 71 } 72} 73 74/** 75 * Delete a news item from ATProto PDS 76 * @param {Object} database - MongoDB connection 77 * @param {string} sub - User sub (auth0|...) 78 * @param {string} rkey - ATProto record key 79 * @returns {Promise<boolean>} 80 */ 81export async function deleteNewsFromAtproto(database, sub, rkey) { 82 const users = database.db.collection("users"); 83 const user = await users.findOne({ _id: sub }); 84 85 if (!user?.atproto?.did || !user?.atproto?.password) { 86 return false; 87 } 88 89 const atprotoDid = user.atproto.did; 90 const atprotoPassword = user.atproto.password; 91 92 try { 93 const agent = new AtpAgent({ service: PDS_URL }); 94 await agent.login({ 95 identifier: atprotoDid, 96 password: atprotoPassword, 97 }); 98 99 await agent.com.atproto.repo.deleteRecord({ 100 repo: atprotoDid, 101 collection: NEWS_COLLECTION, 102 rkey, 103 }); 104 105 shell.log(`🗑️ Deleted ATProto news: ${rkey}`); 106 return true; 107 } catch (error) { 108 shell.error(`❌ Failed to delete news from ATProto: ${error.message}`); 109 return false; 110 } 111} 112 113/** 114 * List news items from a specific user's ATProto repo (public, no auth needed) 115 * @param {string} did - User's ATProto DID 116 * @param {number} limit - Max items to fetch 117 * @param {string} cursor - Pagination cursor 118 * @returns {Promise<{records: Array, cursor?: string}>} 119 */ 120export async function listNewsFromAtproto(did, limit = 50, cursor = null) { 121 if (!did) { 122 return { records: [] }; 123 } 124 125 try { 126 const agent = new AtpAgent({ service: PDS_URL }); 127 128 const params = { 129 repo: did, 130 collection: NEWS_COLLECTION, 131 limit, 132 }; 133 if (cursor) params.cursor = cursor; 134 135 const result = await agent.com.atproto.repo.listRecords(params); 136 137 return { 138 records: result.data?.records || [], 139 cursor: result.data?.cursor, 140 }; 141 } catch (error) { 142 shell.error(`❌ Failed to list news from ATProto: ${error.message}`); 143 return { records: [] }; 144 } 145}