tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
add action to publish lexicons
awarm.space
9 months ago
96c7634b
c8b77aa5
+172
3 changed files
expand all
collapse all
unified
split
.github
workflows
main.yml
lexicons
publish.ts
package.json
+12
.github/workflows/main.yml
···
17
17
cache: "npm"
18
18
- run: "npm i"
19
19
- run: "npx tsc"
20
20
+
21
21
+
lexicons:
22
22
+
runs-on: ubuntu-latest
23
23
+
name: lexicon
24
24
+
steps:
25
25
+
- uses: actions/checkout@v4
26
26
+
- uses: actions/setup-node@v4
27
27
+
with:
28
28
+
node-version: 20
29
29
+
cache: "npm"
30
30
+
- run: "npm i"
31
31
+
- run: "npm run publish-lexicons"
20
32
deploy-supabase:
21
33
needs: [typecheck]
22
34
runs-on: ubuntu-latest
+159
lexicons/publish.ts
···
1
1
+
import * as fs from "fs";
2
2
+
import * as path from "path";
3
3
+
import * as dns from "dns/promises";
4
4
+
import { AtpAgent } from "@atproto/api";
5
5
+
6
6
+
function readLexiconFiles(): { id: string }[] {
7
7
+
const lexiconDir = path.join("lexicons", "pub", "leaflet");
8
8
+
const lexiconFiles: { id: string }[] = [];
9
9
+
10
10
+
function processDirectory(dirPath: string) {
11
11
+
try {
12
12
+
const items = fs.readdirSync(dirPath);
13
13
+
14
14
+
for (const item of items) {
15
15
+
const itemPath = path.join(dirPath, item);
16
16
+
const stats = fs.statSync(itemPath);
17
17
+
18
18
+
if (stats.isFile()) {
19
19
+
try {
20
20
+
const fileContent = fs.readFileSync(itemPath, "utf8");
21
21
+
const jsonData = JSON.parse(fileContent);
22
22
+
lexiconFiles.push(jsonData);
23
23
+
} catch (parseError) {
24
24
+
console.error(
25
25
+
`Error parsing JSON from file ${itemPath}:`,
26
26
+
parseError,
27
27
+
);
28
28
+
}
29
29
+
} else if (stats.isDirectory()) {
30
30
+
processDirectory(itemPath);
31
31
+
}
32
32
+
}
33
33
+
} catch (error) {
34
34
+
console.error(`Error reading directory ${dirPath}:`, error);
35
35
+
}
36
36
+
}
37
37
+
38
38
+
processDirectory(lexiconDir);
39
39
+
40
40
+
return lexiconFiles;
41
41
+
}
42
42
+
// Use the function to get all leaflet JSON files
43
43
+
const lexiconsData = readLexiconFiles();
44
44
+
45
45
+
const agent = new AtpAgent({
46
46
+
service: "https://bsky.social",
47
47
+
});
48
48
+
async function main() {
49
49
+
const VERCEL_TOKEN = process.env.VERCEL_TOKEN;
50
50
+
const LEAFLET_APP_PASSWORD = process.env.LEAFLET_APP_PASSWORD;
51
51
+
if (!LEAFLET_APP_PASSWORD)
52
52
+
throw new Error("Missing env var LEAFLET_APP_PASSWORD");
53
53
+
if (!VERCEL_TOKEN) throw new Error("Missing env var VERCEL_TOKEN");
54
54
+
//login with the agent
55
55
+
await agent.login({
56
56
+
identifier: "leaflet.pub",
57
57
+
password: LEAFLET_APP_PASSWORD,
58
58
+
});
59
59
+
const uniqueIds = Array.from(new Set(lexiconsData.map((lex) => lex.id)));
60
60
+
for (let id of uniqueIds) {
61
61
+
let host = id.split(".").slice(0, -1).reverse().join(".");
62
62
+
host = `_lexicon.${host}`;
63
63
+
let txtRecords = await getTXTRecords(host);
64
64
+
if (!txtRecords.find((r) => r.join("") === agent.assertDid)) {
65
65
+
let name = host.split(".").slice(0, -2).join(".") || "";
66
66
+
console.log("creating txt record", name);
67
67
+
let res = await fetch(
68
68
+
`https://api.vercel.com/v2/domains/leaflet.pub/records?teamId=team_42xaJiZMTw9Sr7i0DcLTae9d`,
69
69
+
{
70
70
+
method: "POST",
71
71
+
headers: {
72
72
+
Authorization: `Bearer ${VERCEL_TOKEN}`,
73
73
+
"Content-Type": "application/json",
74
74
+
},
75
75
+
body: JSON.stringify({
76
76
+
name: name,
77
77
+
type: "TXT",
78
78
+
value: agent.assertDid,
79
79
+
ttl: 60,
80
80
+
}),
81
81
+
},
82
82
+
);
83
83
+
if (res.status !== 200) {
84
84
+
console.log(await res.json());
85
85
+
return;
86
86
+
}
87
87
+
}
88
88
+
}
89
89
+
for (let lex of lexiconsData) {
90
90
+
let record = await getRecord(lex.id, agent);
91
91
+
let newRecord = {
92
92
+
$type: "com.atproto.lexicon.schema",
93
93
+
...lex,
94
94
+
};
95
95
+
if (!record || !deepEquals(record.data.value, newRecord)) {
96
96
+
console.log("putting record", lex.id);
97
97
+
await agent.com.atproto.repo.putRecord({
98
98
+
collection: "com.atproto.lexicon.schema",
99
99
+
repo: agent.assertDid,
100
100
+
rkey: lex.id,
101
101
+
record: newRecord,
102
102
+
});
103
103
+
}
104
104
+
}
105
105
+
}
106
106
+
107
107
+
let getTXTRecords = async (host: string) => {
108
108
+
try {
109
109
+
let txtrecords = await dns.resolveTxt(host);
110
110
+
return txtrecords;
111
111
+
} catch (e) {
112
112
+
return [];
113
113
+
}
114
114
+
};
115
115
+
116
116
+
main();
117
117
+
function deepEquals(obj1: any, obj2: any): boolean {
118
118
+
// Check if both are the same reference
119
119
+
if (obj1 === obj2) return true;
120
120
+
121
121
+
// Check if either is null or not an object
122
122
+
if (
123
123
+
obj1 === null ||
124
124
+
obj2 === null ||
125
125
+
typeof obj1 !== "object" ||
126
126
+
typeof obj2 !== "object"
127
127
+
)
128
128
+
return false;
129
129
+
130
130
+
// Get keys from both objects
131
131
+
const keys1 = Object.keys(obj1);
132
132
+
const keys2 = Object.keys(obj2);
133
133
+
134
134
+
// Check if they have the same number of keys
135
135
+
if (keys1.length !== keys2.length) return false;
136
136
+
137
137
+
// Check each key and value recursively
138
138
+
for (const key of keys1) {
139
139
+
if (!keys2.includes(key)) return false;
140
140
+
if (!deepEquals(obj1[key], obj2[key])) return false;
141
141
+
}
142
142
+
143
143
+
return true;
144
144
+
}
145
145
+
146
146
+
async function getRecord(rkey: string, agent: AtpAgent) {
147
147
+
try {
148
148
+
let record = await agent.com.atproto.repo.getRecord({
149
149
+
collection: "com.atproto.lexicon.schema",
150
150
+
repo: agent.assertDid,
151
151
+
rkey,
152
152
+
});
153
153
+
return record;
154
154
+
} catch (e) {
155
155
+
//@ts-ignore
156
156
+
if (e.error === "RecordNotFound") return null;
157
157
+
throw e;
158
158
+
}
159
159
+
}
+1
package.json
···
5
5
"main": "index.js",
6
6
"scripts": {
7
7
"dev": "next dev --turbo",
8
8
+
"publish-lexicons": "tsx lexicons/publish.ts",
8
9
"generate-db-types": "supabase gen types --local > supabase/database.types.ts && drizzle-kit introspect && rm -rf ./drizzle/*.sql ./drizzle/meta",
9
10
"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/* ./lexicons/pub/leaflet/*/* ./lexicons/com/atproto/*/* --yes && find './lexicons/api' -type f -exec sed -i 's/\\.js'/'/g' {} \\;",
10
11
"wrangler-dev": "wrangler dev",