a tool for shared writing and social publishing
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}