a tool for shared writing and social publishing

add action to publish lexicons

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