a tool for shared writing and social publishing
at feature/footnotes 133 lines 3.8 kB view raw
1import * as fs from "fs"; 2import * as path from "path"; 3import * as dns from "dns/promises"; 4import { AtpAgent } from "@atproto/api"; 5import { deepEquals } from "src/utils/deepEquals"; 6 7function readLexiconFiles(): { id: string }[] { 8 const lexiconDir = path.join("lexicons", "pub", "leaflet"); 9 const lexiconFiles: { id: string }[] = []; 10 11 function processDirectory(dirPath: string) { 12 try { 13 const items = fs.readdirSync(dirPath); 14 15 for (const item of items) { 16 const itemPath = path.join(dirPath, item); 17 const stats = fs.statSync(itemPath); 18 19 if (stats.isFile()) { 20 try { 21 const fileContent = fs.readFileSync(itemPath, "utf8"); 22 const jsonData = JSON.parse(fileContent); 23 lexiconFiles.push(jsonData); 24 } catch (parseError) { 25 console.error( 26 `Error parsing JSON from file ${itemPath}:`, 27 parseError, 28 ); 29 } 30 } else if (stats.isDirectory()) { 31 processDirectory(itemPath); 32 } 33 } 34 } catch (error) { 35 console.error(`Error reading directory ${dirPath}:`, error); 36 } 37 } 38 39 processDirectory(lexiconDir); 40 41 return lexiconFiles; 42} 43// Use the function to get all leaflet JSON files 44const lexiconsData = readLexiconFiles(); 45 46const agent = new AtpAgent({ 47 service: "https://bsky.social", 48}); 49async function main() { 50 const VERCEL_TOKEN = process.env.VERCEL_TOKEN; 51 const LEAFLET_APP_PASSWORD = process.env.LEAFLET_APP_PASSWORD; 52 if (!LEAFLET_APP_PASSWORD) 53 throw new Error("Missing env var LEAFLET_APP_PASSWORD"); 54 if (!VERCEL_TOKEN) throw new Error("Missing env var VERCEL_TOKEN"); 55 //login with the agent 56 await agent.login({ 57 identifier: "leaflet.pub", 58 password: LEAFLET_APP_PASSWORD, 59 }); 60 const uniqueIds = Array.from(new Set(lexiconsData.map((lex) => lex.id))); 61 let txtRecordValue = `did=${agent.assertDid}`; 62 for (let id of uniqueIds) { 63 let host = id.split(".").slice(0, -1).reverse().join("."); 64 host = `_lexicon.${host}`; 65 let txtRecords = await getTXTRecords(host); 66 if (!txtRecords.find((r) => r.join("") === txtRecordValue)) { 67 let name = host.split(".").slice(0, -2).join(".") || ""; 68 console.log("creating txt record", name); 69 let res = await fetch( 70 `https://api.vercel.com/v2/domains/leaflet.pub/records?teamId=team_42xaJiZMTw9Sr7i0DcLTae9d`, 71 { 72 method: "POST", 73 headers: { 74 Authorization: `Bearer ${VERCEL_TOKEN}`, 75 "Content-Type": "application/json", 76 }, 77 body: JSON.stringify({ 78 name: name, 79 type: "TXT", 80 value: txtRecordValue, 81 ttl: 60, 82 }), 83 }, 84 ); 85 if (res.status !== 200) { 86 console.log(await res.json()); 87 return; 88 } 89 } 90 } 91 for (let lex of lexiconsData) { 92 let record = await getRecord(lex.id, agent); 93 let newRecord = { 94 $type: "com.atproto.lexicon.schema", 95 ...lex, 96 }; 97 if (!record || !deepEquals(record.data.value, newRecord)) { 98 console.log("putting record", lex.id); 99 await agent.com.atproto.repo.putRecord({ 100 collection: "com.atproto.lexicon.schema", 101 repo: agent.assertDid, 102 rkey: lex.id, 103 record: newRecord, 104 }); 105 } 106 } 107} 108 109let getTXTRecords = async (host: string) => { 110 try { 111 let txtrecords = await dns.resolveTxt(host); 112 return txtrecords; 113 } catch (e) { 114 return []; 115 } 116}; 117 118main(); 119 120async function getRecord(rkey: string, agent: AtpAgent) { 121 try { 122 let record = await agent.com.atproto.repo.getRecord({ 123 collection: "com.atproto.lexicon.schema", 124 repo: agent.assertDid, 125 rkey, 126 }); 127 return record; 128 } catch (e) { 129 //@ts-ignore 130 if (e.error === "RecordNotFound") return null; 131 throw e; 132 } 133}