Monorepo for Aesthetic.Computer
aesthetic.computer
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}