atproto pastebin service: https://plonk.li

Compare changes

Choose any two refs to compare.

+86 -1
flake.nix
··· 10 10 supportedSystems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"]; 11 11 forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 12 12 nixpkgsFor = forAllSystems (system: 13 - import nixpkgs { inherit system; }); 13 + import nixpkgs { 14 + inherit system; 15 + overlays = [self.overlays.default]; 16 + }); 14 17 in { 18 + overlays.default = final: prev: let 19 + pname = "plonk"; 20 + version = "0.1.0"; 21 + in { 22 + plonk = with final; 23 + buildNpmPackage { 24 + inherit pname version; 25 + src = ./.; 26 + nativeBuildInputs = [makeBinaryWrapper]; 27 + packageJson = ./package.json; 28 + buildPhase = "npm run build"; 29 + installPhase = '' 30 + runHook preInstall 31 + 32 + mkdir -p $out/bin 33 + cp -r ./* $out/ 34 + 35 + makeBinaryWrapper ${nodejs}/bin/node $out/bin/$pname \ 36 + --prefix PATH : ${lib.makeBinPath [nodejs]} \ 37 + --add-flags "$out/dist/index.js" 38 + ''; 39 + 40 + npmDepsHash = "sha256-qGCbaFAHd/s9hOTWMjHCam6Kf6pU6IWPybfwYh0sOwc="; 41 + }; 42 + }; 43 + 44 + packages = forAllSystems (system: { 45 + inherit (nixpkgsFor."${system}") plonk; 46 + }); 47 + 48 + defaultPackage = forAllSystems (system: nixpkgsFor."${system}".plonk); 15 49 16 50 devShell = forAllSystems (system: let 17 51 pkgs = nixpkgsFor."${system}"; ··· 26 60 }); 27 61 28 62 formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 63 + 64 + nixosModules.default = { 65 + config, 66 + pkgs, 67 + lib, 68 + ... 69 + }: 70 + with lib; { 71 + options = { 72 + services.plonk = { 73 + enable = mkOption { 74 + type = types.bool; 75 + default = false; 76 + description = "Enable plonk"; 77 + }; 78 + port = mkOption { 79 + type = types.int; 80 + default = 3000; 81 + description = "Port to run plonk on"; 82 + }; 83 + cookie_secret = mkOption { 84 + type = types.str; 85 + default = "00000000000000000000000000000000"; 86 + description = "Cookie secret"; 87 + }; 88 + }; 89 + }; 90 + 91 + config = mkIf config.services.plonk.enable { 92 + nixpkgs.overlays = [self.overlays.default]; 93 + systemd.services.plonk = { 94 + description = "plonk service"; 95 + wantedBy = ["multi-user.target"]; 96 + 97 + serviceConfig = { 98 + ListenStream = "0.0.0.0:${toString config.services.plonk.port}"; 99 + ExecStart = "${pkgs.plonk}/bin/plonk"; 100 + Restart = "always"; 101 + }; 102 + 103 + environment = { 104 + PLONK_PORT = "${toString config.services.plonk.port}"; 105 + PLONK_NODE_ENV = "production"; 106 + PLONK_HOST = "localhost"; 107 + PLONK_PUBLIC_URL = "https://plonk.li"; 108 + PLONK_DB_PATH = "plonk.db"; 109 + PLONK_COOKIE_SECRET = config.services.plonk.cookie_secret; 110 + }; 111 + }; 112 + }; 113 + }; 29 114 }; 30 115 }
+35
lexicons/comment.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "li.plonk.comment", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": [ 11 + "content", 12 + "createdAt", 13 + "post" 14 + ], 15 + "properties": { 16 + "content": { 17 + "type": "string", 18 + "maxLength": 100000, 19 + "maxGraphemes": 10000, 20 + "description": "comment body" 21 + }, 22 + "createdAt": { 23 + "type": "string", 24 + "format": "datetime", 25 + "description": "comment creation timestamp" 26 + }, 27 + "post": { 28 + "type": "ref", 29 + "ref": "com.atproto.repo.strongRef" 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }
+67
lexicons/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single record from a repository. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "repo", 12 + "collection", 13 + "rkey" 14 + ], 15 + "properties": { 16 + "repo": { 17 + "type": "string", 18 + "format": "at-identifier", 19 + "description": "The handle or DID of the repo." 20 + }, 21 + "collection": { 22 + "type": "string", 23 + "format": "nsid", 24 + "description": "The NSID of the record collection." 25 + }, 26 + "rkey": { 27 + "type": "string", 28 + "description": "The Record Key." 29 + }, 30 + "cid": { 31 + "type": "string", 32 + "format": "cid", 33 + "description": "The CID of the version of the record. If not specified, then return the most recent version." 34 + } 35 + } 36 + }, 37 + "output": { 38 + "encoding": "application/json", 39 + "schema": { 40 + "type": "object", 41 + "required": [ 42 + "uri", 43 + "value" 44 + ], 45 + "properties": { 46 + "uri": { 47 + "type": "string", 48 + "format": "at-uri" 49 + }, 50 + "cid": { 51 + "type": "string", 52 + "format": "cid" 53 + }, 54 + "value": { 55 + "type": "unknown" 56 + } 57 + } 58 + } 59 + }, 60 + "errors": [ 61 + { 62 + "name": "RecordNotFound" 63 + } 64 + ] 65 + } 66 + } 67 + }
+93
lexicons/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List a range of records in a repository, matching a specific collection. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "repo", 12 + "collection" 13 + ], 14 + "properties": { 15 + "repo": { 16 + "type": "string", 17 + "format": "at-identifier", 18 + "description": "The handle or DID of the repo." 19 + }, 20 + "collection": { 21 + "type": "string", 22 + "format": "nsid", 23 + "description": "The NSID of the record type." 24 + }, 25 + "limit": { 26 + "type": "integer", 27 + "minimum": 1, 28 + "maximum": 100, 29 + "default": 50, 30 + "description": "The number of records to return." 31 + }, 32 + "cursor": { 33 + "type": "string" 34 + }, 35 + "rkeyStart": { 36 + "type": "string", 37 + "description": "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)" 38 + }, 39 + "rkeyEnd": { 40 + "type": "string", 41 + "description": "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)" 42 + }, 43 + "reverse": { 44 + "type": "boolean", 45 + "description": "Flag to reverse the order of the returned records." 46 + } 47 + } 48 + }, 49 + "output": { 50 + "encoding": "application/json", 51 + "schema": { 52 + "type": "object", 53 + "required": [ 54 + "records" 55 + ], 56 + "properties": { 57 + "cursor": { 58 + "type": "string" 59 + }, 60 + "records": { 61 + "type": "array", 62 + "items": { 63 + "type": "ref", 64 + "ref": "#record" 65 + } 66 + } 67 + } 68 + } 69 + } 70 + }, 71 + "record": { 72 + "type": "object", 73 + "required": [ 74 + "uri", 75 + "cid", 76 + "value" 77 + ], 78 + "properties": { 79 + "uri": { 80 + "type": "string", 81 + "format": "at-uri" 82 + }, 83 + "cid": { 84 + "type": "string", 85 + "format": "cid" 86 + }, 87 + "value": { 88 + "type": "unknown" 89 + } 90 + } 91 + } 92 + } 93 + }
+18 -3
lexicons/paste.json
··· 1 1 { 2 2 "lexicon": 1, 3 - "id": "ovh.plonk.paste", 3 + "id": "li.plonk.paste", 4 4 "defs": { 5 5 "main": { 6 6 "type": "record", 7 7 "key": "tid", 8 8 "record": { 9 9 "type": "object", 10 - "required": ["code", "lang", "title", "createdAt"], 10 + "required": [ 11 + "code", 12 + "shortUrl", 13 + "lang", 14 + "title", 15 + "createdAt" 16 + ], 11 17 "properties": { 12 18 "code": { 13 19 "type": "string", ··· 15 21 "maxGraphemes": 65536, 16 22 "maxLength": 65536 17 23 }, 24 + "shortUrl": { 25 + "type": "string", 26 + "minLength": 2, 27 + "maxGraphemes": 10, 28 + "maxLength": 10 29 + }, 18 30 "lang": { 19 31 "type": "string", 20 32 "minLength": 1, ··· 27 39 "maxGraphemes": 100, 28 40 "maxLength": 100 29 41 }, 30 - "createdAt": { "type": "string", "format": "datetime" } 42 + "createdAt": { 43 + "type": "string", 44 + "format": "datetime" 45 + } 31 46 } 32 47 } 33 48 }
+523
package-lock.json
··· 26 26 "multiformats": "^9.9.0", 27 27 "pino": "^9.3.2", 28 28 "pug": "^3.0.3", 29 + "shiki": "^3.4.2", 29 30 "uhtml": "^4.5.9" 30 31 }, 31 32 "devDependencies": { ··· 1484 1485 "win32" 1485 1486 ] 1486 1487 }, 1488 + "node_modules/@shikijs/core": { 1489 + "version": "3.4.2", 1490 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.4.2.tgz", 1491 + "integrity": "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ==", 1492 + "license": "MIT", 1493 + "dependencies": { 1494 + "@shikijs/types": "3.4.2", 1495 + "@shikijs/vscode-textmate": "^10.0.2", 1496 + "@types/hast": "^3.0.4", 1497 + "hast-util-to-html": "^9.0.5" 1498 + } 1499 + }, 1500 + "node_modules/@shikijs/engine-javascript": { 1501 + "version": "3.4.2", 1502 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.4.2.tgz", 1503 + "integrity": "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ==", 1504 + "license": "MIT", 1505 + "dependencies": { 1506 + "@shikijs/types": "3.4.2", 1507 + "@shikijs/vscode-textmate": "^10.0.2", 1508 + "oniguruma-to-es": "^4.3.3" 1509 + } 1510 + }, 1511 + "node_modules/@shikijs/engine-oniguruma": { 1512 + "version": "3.4.2", 1513 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz", 1514 + "integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==", 1515 + "license": "MIT", 1516 + "dependencies": { 1517 + "@shikijs/types": "3.4.2", 1518 + "@shikijs/vscode-textmate": "^10.0.2" 1519 + } 1520 + }, 1521 + "node_modules/@shikijs/langs": { 1522 + "version": "3.4.2", 1523 + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz", 1524 + "integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==", 1525 + "license": "MIT", 1526 + "dependencies": { 1527 + "@shikijs/types": "3.4.2" 1528 + } 1529 + }, 1530 + "node_modules/@shikijs/themes": { 1531 + "version": "3.4.2", 1532 + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz", 1533 + "integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==", 1534 + "license": "MIT", 1535 + "dependencies": { 1536 + "@shikijs/types": "3.4.2" 1537 + } 1538 + }, 1539 + "node_modules/@shikijs/types": { 1540 + "version": "3.4.2", 1541 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz", 1542 + "integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==", 1543 + "license": "MIT", 1544 + "dependencies": { 1545 + "@shikijs/vscode-textmate": "^10.0.2", 1546 + "@types/hast": "^3.0.4" 1547 + } 1548 + }, 1549 + "node_modules/@shikijs/vscode-textmate": { 1550 + "version": "10.0.2", 1551 + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", 1552 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", 1553 + "license": "MIT" 1554 + }, 1487 1555 "node_modules/@ts-morph/common": { 1488 1556 "version": "0.17.0", 1489 1557 "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.17.0.tgz", ··· 1602 1670 "@types/send": "*" 1603 1671 } 1604 1672 }, 1673 + "node_modules/@types/hast": { 1674 + "version": "3.0.4", 1675 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", 1676 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 1677 + "license": "MIT", 1678 + "dependencies": { 1679 + "@types/unist": "*" 1680 + } 1681 + }, 1605 1682 "node_modules/@types/http-errors": { 1606 1683 "version": "2.0.4", 1607 1684 "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 1608 1685 "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 1609 1686 "dev": true, 1610 1687 "license": "MIT" 1688 + }, 1689 + "node_modules/@types/mdast": { 1690 + "version": "4.0.4", 1691 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", 1692 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 1693 + "license": "MIT", 1694 + "dependencies": { 1695 + "@types/unist": "*" 1696 + } 1611 1697 }, 1612 1698 "node_modules/@types/mime": { 1613 1699 "version": "1.3.5", ··· 1662 1748 "@types/node": "*", 1663 1749 "@types/send": "*" 1664 1750 } 1751 + }, 1752 + "node_modules/@types/unist": { 1753 + "version": "3.0.3", 1754 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", 1755 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", 1756 + "license": "MIT" 1757 + }, 1758 + "node_modules/@ungap/structured-clone": { 1759 + "version": "1.3.0", 1760 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", 1761 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", 1762 + "license": "ISC" 1665 1763 }, 1666 1764 "node_modules/@webreflection/signal": { 1667 1765 "version": "2.1.2", ··· 2095 2193 "cborg": "cli.js" 2096 2194 } 2097 2195 }, 2196 + "node_modules/ccount": { 2197 + "version": "2.0.1", 2198 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", 2199 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", 2200 + "license": "MIT", 2201 + "funding": { 2202 + "type": "github", 2203 + "url": "https://github.com/sponsors/wooorm" 2204 + } 2205 + }, 2098 2206 "node_modules/chalk": { 2099 2207 "version": "4.1.2", 2100 2208 "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", ··· 2110 2218 }, 2111 2219 "funding": { 2112 2220 "url": "https://github.com/chalk/chalk?sponsor=1" 2221 + } 2222 + }, 2223 + "node_modules/character-entities-html4": { 2224 + "version": "2.1.0", 2225 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", 2226 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", 2227 + "license": "MIT", 2228 + "funding": { 2229 + "type": "github", 2230 + "url": "https://github.com/sponsors/wooorm" 2231 + } 2232 + }, 2233 + "node_modules/character-entities-legacy": { 2234 + "version": "3.0.0", 2235 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", 2236 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", 2237 + "license": "MIT", 2238 + "funding": { 2239 + "type": "github", 2240 + "url": "https://github.com/sponsors/wooorm" 2113 2241 } 2114 2242 }, 2115 2243 "node_modules/character-parser": { ··· 2189 2317 "node": ">= 0.8" 2190 2318 } 2191 2319 }, 2320 + "node_modules/comma-separated-tokens": { 2321 + "version": "2.0.3", 2322 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", 2323 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", 2324 + "license": "MIT", 2325 + "funding": { 2326 + "type": "github", 2327 + "url": "https://github.com/sponsors/wooorm" 2328 + } 2329 + }, 2192 2330 "node_modules/commander": { 2193 2331 "version": "9.5.0", 2194 2332 "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", ··· 2361 2499 "node": ">= 0.8" 2362 2500 } 2363 2501 }, 2502 + "node_modules/dequal": { 2503 + "version": "2.0.3", 2504 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 2505 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 2506 + "license": "MIT", 2507 + "engines": { 2508 + "node": ">=6" 2509 + } 2510 + }, 2364 2511 "node_modules/destroy": { 2365 2512 "version": "1.2.0", 2366 2513 "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", ··· 2378 2525 "license": "Apache-2.0", 2379 2526 "engines": { 2380 2527 "node": ">=8" 2528 + } 2529 + }, 2530 + "node_modules/devlop": { 2531 + "version": "1.1.0", 2532 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", 2533 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 2534 + "license": "MIT", 2535 + "dependencies": { 2536 + "dequal": "^2.0.0" 2537 + }, 2538 + "funding": { 2539 + "type": "github", 2540 + "url": "https://github.com/sponsors/wooorm" 2381 2541 } 2382 2542 }, 2383 2543 "node_modules/diff": { ··· 3043 3203 "node": ">= 0.4" 3044 3204 } 3045 3205 }, 3206 + "node_modules/hast-util-to-html": { 3207 + "version": "9.0.5", 3208 + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", 3209 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 3210 + "license": "MIT", 3211 + "dependencies": { 3212 + "@types/hast": "^3.0.0", 3213 + "@types/unist": "^3.0.0", 3214 + "ccount": "^2.0.0", 3215 + "comma-separated-tokens": "^2.0.0", 3216 + "hast-util-whitespace": "^3.0.0", 3217 + "html-void-elements": "^3.0.0", 3218 + "mdast-util-to-hast": "^13.0.0", 3219 + "property-information": "^7.0.0", 3220 + "space-separated-tokens": "^2.0.0", 3221 + "stringify-entities": "^4.0.0", 3222 + "zwitch": "^2.0.4" 3223 + }, 3224 + "funding": { 3225 + "type": "opencollective", 3226 + "url": "https://opencollective.com/unified" 3227 + } 3228 + }, 3229 + "node_modules/hast-util-whitespace": { 3230 + "version": "3.0.0", 3231 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", 3232 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", 3233 + "license": "MIT", 3234 + "dependencies": { 3235 + "@types/hast": "^3.0.0" 3236 + }, 3237 + "funding": { 3238 + "type": "opencollective", 3239 + "url": "https://opencollective.com/unified" 3240 + } 3241 + }, 3046 3242 "node_modules/help-me": { 3047 3243 "version": "5.0.0", 3048 3244 "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", ··· 3056 3252 "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", 3057 3253 "license": "MIT" 3058 3254 }, 3255 + "node_modules/html-void-elements": { 3256 + "version": "3.0.0", 3257 + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", 3258 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", 3259 + "license": "MIT", 3260 + "funding": { 3261 + "type": "github", 3262 + "url": "https://github.com/sponsors/wooorm" 3263 + } 3264 + }, 3059 3265 "node_modules/htmlparser2": { 3060 3266 "version": "9.1.0", 3061 3267 "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", ··· 3404 3610 "dev": true, 3405 3611 "license": "ISC" 3406 3612 }, 3613 + "node_modules/mdast-util-to-hast": { 3614 + "version": "13.2.0", 3615 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", 3616 + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", 3617 + "license": "MIT", 3618 + "dependencies": { 3619 + "@types/hast": "^3.0.0", 3620 + "@types/mdast": "^4.0.0", 3621 + "@ungap/structured-clone": "^1.0.0", 3622 + "devlop": "^1.0.0", 3623 + "micromark-util-sanitize-uri": "^2.0.0", 3624 + "trim-lines": "^3.0.0", 3625 + "unist-util-position": "^5.0.0", 3626 + "unist-util-visit": "^5.0.0", 3627 + "vfile": "^6.0.0" 3628 + }, 3629 + "funding": { 3630 + "type": "opencollective", 3631 + "url": "https://opencollective.com/unified" 3632 + } 3633 + }, 3407 3634 "node_modules/media-typer": { 3408 3635 "version": "0.3.0", 3409 3636 "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", ··· 3441 3668 "node": ">= 0.6" 3442 3669 } 3443 3670 }, 3671 + "node_modules/micromark-util-character": { 3672 + "version": "2.1.1", 3673 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", 3674 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 3675 + "funding": [ 3676 + { 3677 + "type": "GitHub Sponsors", 3678 + "url": "https://github.com/sponsors/unifiedjs" 3679 + }, 3680 + { 3681 + "type": "OpenCollective", 3682 + "url": "https://opencollective.com/unified" 3683 + } 3684 + ], 3685 + "license": "MIT", 3686 + "dependencies": { 3687 + "micromark-util-symbol": "^2.0.0", 3688 + "micromark-util-types": "^2.0.0" 3689 + } 3690 + }, 3691 + "node_modules/micromark-util-encode": { 3692 + "version": "2.0.1", 3693 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", 3694 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", 3695 + "funding": [ 3696 + { 3697 + "type": "GitHub Sponsors", 3698 + "url": "https://github.com/sponsors/unifiedjs" 3699 + }, 3700 + { 3701 + "type": "OpenCollective", 3702 + "url": "https://opencollective.com/unified" 3703 + } 3704 + ], 3705 + "license": "MIT" 3706 + }, 3707 + "node_modules/micromark-util-sanitize-uri": { 3708 + "version": "2.0.1", 3709 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", 3710 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 3711 + "funding": [ 3712 + { 3713 + "type": "GitHub Sponsors", 3714 + "url": "https://github.com/sponsors/unifiedjs" 3715 + }, 3716 + { 3717 + "type": "OpenCollective", 3718 + "url": "https://opencollective.com/unified" 3719 + } 3720 + ], 3721 + "license": "MIT", 3722 + "dependencies": { 3723 + "micromark-util-character": "^2.0.0", 3724 + "micromark-util-encode": "^2.0.0", 3725 + "micromark-util-symbol": "^2.0.0" 3726 + } 3727 + }, 3728 + "node_modules/micromark-util-symbol": { 3729 + "version": "2.0.1", 3730 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", 3731 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", 3732 + "funding": [ 3733 + { 3734 + "type": "GitHub Sponsors", 3735 + "url": "https://github.com/sponsors/unifiedjs" 3736 + }, 3737 + { 3738 + "type": "OpenCollective", 3739 + "url": "https://opencollective.com/unified" 3740 + } 3741 + ], 3742 + "license": "MIT" 3743 + }, 3744 + "node_modules/micromark-util-types": { 3745 + "version": "2.0.2", 3746 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", 3747 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", 3748 + "funding": [ 3749 + { 3750 + "type": "GitHub Sponsors", 3751 + "url": "https://github.com/sponsors/unifiedjs" 3752 + }, 3753 + { 3754 + "type": "OpenCollective", 3755 + "url": "https://opencollective.com/unified" 3756 + } 3757 + ], 3758 + "license": "MIT" 3759 + }, 3444 3760 "node_modules/micromatch": { 3445 3761 "version": "4.0.8", 3446 3762 "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", ··· 3671 3987 "wrappy": "1" 3672 3988 } 3673 3989 }, 3990 + "node_modules/oniguruma-parser": { 3991 + "version": "0.12.1", 3992 + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", 3993 + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", 3994 + "license": "MIT" 3995 + }, 3996 + "node_modules/oniguruma-to-es": { 3997 + "version": "4.3.3", 3998 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz", 3999 + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", 4000 + "license": "MIT", 4001 + "dependencies": { 4002 + "oniguruma-parser": "^0.12.1", 4003 + "regex": "^6.0.1", 4004 + "regex-recursion": "^6.0.2" 4005 + } 4006 + }, 3674 4007 "node_modules/p-finally": { 3675 4008 "version": "1.0.0", 3676 4009 "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", ··· 3972 4305 "asap": "~2.0.3" 3973 4306 } 3974 4307 }, 4308 + "node_modules/property-information": { 4309 + "version": "7.1.0", 4310 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", 4311 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", 4312 + "license": "MIT", 4313 + "funding": { 4314 + "type": "github", 4315 + "url": "https://github.com/sponsors/wooorm" 4316 + } 4317 + }, 3975 4318 "node_modules/proxy-addr": { 3976 4319 "version": "2.0.7", 3977 4320 "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", ··· 4284 4627 "node": ">= 12.13.0" 4285 4628 } 4286 4629 }, 4630 + "node_modules/regex": { 4631 + "version": "6.0.1", 4632 + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", 4633 + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", 4634 + "license": "MIT", 4635 + "dependencies": { 4636 + "regex-utilities": "^2.3.0" 4637 + } 4638 + }, 4639 + "node_modules/regex-recursion": { 4640 + "version": "6.0.2", 4641 + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", 4642 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", 4643 + "license": "MIT", 4644 + "dependencies": { 4645 + "regex-utilities": "^2.3.0" 4646 + } 4647 + }, 4648 + "node_modules/regex-utilities": { 4649 + "version": "2.3.0", 4650 + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", 4651 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", 4652 + "license": "MIT" 4653 + }, 4287 4654 "node_modules/resolve": { 4288 4655 "version": "1.22.8", 4289 4656 "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", ··· 4564 4931 "node": ">=8" 4565 4932 } 4566 4933 }, 4934 + "node_modules/shiki": { 4935 + "version": "3.4.2", 4936 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.4.2.tgz", 4937 + "integrity": "sha512-wuxzZzQG8kvZndD7nustrNFIKYJ1jJoWIPaBpVe2+KHSvtzMi4SBjOxrigs8qeqce/l3U0cwiC+VAkLKSunHQQ==", 4938 + "license": "MIT", 4939 + "dependencies": { 4940 + "@shikijs/core": "3.4.2", 4941 + "@shikijs/engine-javascript": "3.4.2", 4942 + "@shikijs/engine-oniguruma": "3.4.2", 4943 + "@shikijs/langs": "3.4.2", 4944 + "@shikijs/themes": "3.4.2", 4945 + "@shikijs/types": "3.4.2", 4946 + "@shikijs/vscode-textmate": "^10.0.2", 4947 + "@types/hast": "^3.0.4" 4948 + } 4949 + }, 4567 4950 "node_modules/side-channel": { 4568 4951 "version": "1.0.6", 4569 4952 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", ··· 4660 5043 }, 4661 5044 "engines": { 4662 5045 "node": ">= 8" 5046 + } 5047 + }, 5048 + "node_modules/space-separated-tokens": { 5049 + "version": "2.0.2", 5050 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", 5051 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", 5052 + "license": "MIT", 5053 + "funding": { 5054 + "type": "github", 5055 + "url": "https://github.com/sponsors/wooorm" 4663 5056 } 4664 5057 }, 4665 5058 "node_modules/split2": { ··· 4753 5146 "node": ">=8" 4754 5147 } 4755 5148 }, 5149 + "node_modules/stringify-entities": { 5150 + "version": "4.0.4", 5151 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", 5152 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", 5153 + "license": "MIT", 5154 + "dependencies": { 5155 + "character-entities-html4": "^2.0.0", 5156 + "character-entities-legacy": "^3.0.0" 5157 + }, 5158 + "funding": { 5159 + "type": "github", 5160 + "url": "https://github.com/sponsors/wooorm" 5161 + } 5162 + }, 4756 5163 "node_modules/strip-ansi": { 4757 5164 "version": "7.1.0", 4758 5165 "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", ··· 5042 5449 "license": "MIT", 5043 5450 "bin": { 5044 5451 "tree-kill": "cli.js" 5452 + } 5453 + }, 5454 + "node_modules/trim-lines": { 5455 + "version": "3.0.1", 5456 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", 5457 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", 5458 + "license": "MIT", 5459 + "funding": { 5460 + "type": "github", 5461 + "url": "https://github.com/sponsors/wooorm" 5045 5462 } 5046 5463 }, 5047 5464 "node_modules/ts-interface-checker": { ··· 5752 6169 "dev": true, 5753 6170 "license": "MIT" 5754 6171 }, 6172 + "node_modules/unist-util-is": { 6173 + "version": "6.0.0", 6174 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", 6175 + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", 6176 + "license": "MIT", 6177 + "dependencies": { 6178 + "@types/unist": "^3.0.0" 6179 + }, 6180 + "funding": { 6181 + "type": "opencollective", 6182 + "url": "https://opencollective.com/unified" 6183 + } 6184 + }, 6185 + "node_modules/unist-util-position": { 6186 + "version": "5.0.0", 6187 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", 6188 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 6189 + "license": "MIT", 6190 + "dependencies": { 6191 + "@types/unist": "^3.0.0" 6192 + }, 6193 + "funding": { 6194 + "type": "opencollective", 6195 + "url": "https://opencollective.com/unified" 6196 + } 6197 + }, 6198 + "node_modules/unist-util-stringify-position": { 6199 + "version": "4.0.0", 6200 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", 6201 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 6202 + "license": "MIT", 6203 + "dependencies": { 6204 + "@types/unist": "^3.0.0" 6205 + }, 6206 + "funding": { 6207 + "type": "opencollective", 6208 + "url": "https://opencollective.com/unified" 6209 + } 6210 + }, 6211 + "node_modules/unist-util-visit": { 6212 + "version": "5.0.0", 6213 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", 6214 + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 6215 + "license": "MIT", 6216 + "dependencies": { 6217 + "@types/unist": "^3.0.0", 6218 + "unist-util-is": "^6.0.0", 6219 + "unist-util-visit-parents": "^6.0.0" 6220 + }, 6221 + "funding": { 6222 + "type": "opencollective", 6223 + "url": "https://opencollective.com/unified" 6224 + } 6225 + }, 6226 + "node_modules/unist-util-visit-parents": { 6227 + "version": "6.0.1", 6228 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", 6229 + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", 6230 + "license": "MIT", 6231 + "dependencies": { 6232 + "@types/unist": "^3.0.0", 6233 + "unist-util-is": "^6.0.0" 6234 + }, 6235 + "funding": { 6236 + "type": "opencollective", 6237 + "url": "https://opencollective.com/unified" 6238 + } 6239 + }, 5755 6240 "node_modules/unpipe": { 5756 6241 "version": "1.0.0", 5757 6242 "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", ··· 5796 6281 "license": "MIT", 5797 6282 "engines": { 5798 6283 "node": ">= 0.8" 6284 + } 6285 + }, 6286 + "node_modules/vfile": { 6287 + "version": "6.0.3", 6288 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", 6289 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 6290 + "license": "MIT", 6291 + "dependencies": { 6292 + "@types/unist": "^3.0.0", 6293 + "vfile-message": "^4.0.0" 6294 + }, 6295 + "funding": { 6296 + "type": "opencollective", 6297 + "url": "https://opencollective.com/unified" 6298 + } 6299 + }, 6300 + "node_modules/vfile-message": { 6301 + "version": "4.0.2", 6302 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", 6303 + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", 6304 + "license": "MIT", 6305 + "dependencies": { 6306 + "@types/unist": "^3.0.0", 6307 + "unist-util-stringify-position": "^4.0.0" 6308 + }, 6309 + "funding": { 6310 + "type": "opencollective", 6311 + "url": "https://opencollective.com/unified" 5799 6312 } 5800 6313 }, 5801 6314 "node_modules/void-elements": { ··· 6003 6516 "license": "MIT", 6004 6517 "funding": { 6005 6518 "url": "https://github.com/sponsors/colinhacks" 6519 + } 6520 + }, 6521 + "node_modules/zwitch": { 6522 + "version": "2.0.4", 6523 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", 6524 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", 6525 + "license": "MIT", 6526 + "funding": { 6527 + "type": "github", 6528 + "url": "https://github.com/sponsors/wooorm" 6006 6529 } 6007 6530 } 6008 6531 }
+5
package.json
··· 30 30 "multiformats": "^9.9.0", 31 31 "pino": "^9.3.2", 32 32 "pug": "^3.0.3", 33 + "shiki": "^3.4.2", 33 34 "uhtml": "^4.5.9" 34 35 }, 35 36 "devDependencies": { ··· 49 50 "!src/**/__tests__/**", 50 51 "!src/**/*.test.*" 51 52 ], 53 + "loader": { 54 + ".pug": "copy", 55 + ".woff2": "copy" 56 + }, 52 57 "splitting": false, 53 58 "sourcemap": true, 54 59 "clean": true
src/assets/NerdIosevka-Regular.woff2

This is a binary file and will not be displayed.

+4 -4
src/auth/client.ts
··· 4 4 import { SessionStore, StateStore } from "./storage"; 5 5 6 6 export const createClient = async (db: Database) => { 7 - const publicUrl = env.PUBLIC_URL; 8 - const url = publicUrl || `http://127.0.0.1:${env.PORT}`; 7 + const publicUrl = env.PLONK_PUBLIC_URL; 8 + const url = publicUrl || `http://127.0.0.1:${env.PLONK_PORT}`; 9 9 const enc = encodeURIComponent; 10 10 return new NodeOAuthClient({ 11 11 clientMetadata: { 12 - client_name: "AT Protocol Express App", 12 + client_name: "plonk.li", 13 13 client_id: publicUrl 14 14 ? `${url}/client-metadata.json` 15 - : `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc("atproto transition:generic")}`, 15 + : `http://${env.PLONK_HOST}?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc("atproto transition:generic")}`, 16 16 client_uri: url, 17 17 redirect_uris: [`${url}/oauth/callback`], 18 18 scope: "atproto transition:generic",
+48 -41
src/auth/storage.ts
··· 1 1 import type { 2 - NodeSavedSession, 3 - NodeSavedSessionStore, 4 - NodeSavedState, 5 - NodeSavedStateStore, 6 - } from '@atproto/oauth-client-node' 7 - import type { Database } from '#/db' 2 + NodeSavedSession, 3 + NodeSavedSessionStore, 4 + NodeSavedState, 5 + NodeSavedStateStore, 6 + } from "@atproto/oauth-client-node"; 7 + import type { Database } from "#/db"; 8 8 9 9 export class StateStore implements NodeSavedStateStore { 10 - constructor(private db: Database) {} 11 - async get(key: string): Promise<NodeSavedState | undefined> { 12 - const result = await this.db.selectFrom('auth_state').selectAll().where('key', '=', key).executeTakeFirst() 13 - if (!result) return 14 - return JSON.parse(result.state) as NodeSavedState 15 - } 16 - async set(key: string, val: NodeSavedState) { 17 - const state = JSON.stringify(val) 18 - await this.db 19 - .insertInto('auth_state') 20 - .values({ key, state }) 21 - .onConflict((oc) => oc.doUpdateSet({ state })) 22 - .execute() 23 - } 24 - async del(key: string) { 25 - await this.db.deleteFrom('auth_state').where('key', '=', key).execute() 26 - } 10 + constructor(private db: Database) {} 11 + async get(key: string): Promise<NodeSavedState | undefined> { 12 + const result = await this.db 13 + .selectFrom("auth_state") 14 + .selectAll() 15 + .where("key", "=", key) 16 + .executeTakeFirst(); 17 + if (!result) return; 18 + return JSON.parse(result.state) as NodeSavedState; 19 + } 20 + async set(key: string, val: NodeSavedState) { 21 + const state = JSON.stringify(val); 22 + await this.db 23 + .insertInto("auth_state") 24 + .values({ key, state }) 25 + .onConflict((oc) => oc.doUpdateSet({ state })) 26 + .execute(); 27 + } 28 + async del(key: string) { 29 + await this.db.deleteFrom("auth_state").where("key", "=", key).execute(); 30 + } 27 31 } 28 32 29 33 export class SessionStore implements NodeSavedSessionStore { 30 - constructor(private db: Database) {} 31 - async get(key: string): Promise<NodeSavedSession | undefined> { 32 - const result = await this.db.selectFrom('auth_session').selectAll().where('key', '=', key).executeTakeFirst() 33 - if (!result) return 34 - return JSON.parse(result.session) as NodeSavedSession 35 - } 36 - async set(key: string, val: NodeSavedSession) { 37 - const session = JSON.stringify(val) 38 - await this.db 39 - .insertInto('auth_session') 40 - .values({ key, session }) 41 - .onConflict((oc) => oc.doUpdateSet({ session })) 42 - .execute() 43 - } 44 - async del(key: string) { 45 - await this.db.deleteFrom('auth_session').where('key', '=', key).execute() 46 - } 34 + constructor(private db: Database) {} 35 + async get(key: string): Promise<NodeSavedSession | undefined> { 36 + const result = await this.db 37 + .selectFrom("auth_session") 38 + .selectAll() 39 + .where("key", "=", key) 40 + .executeTakeFirst(); 41 + if (!result) return; 42 + return JSON.parse(result.session) as NodeSavedSession; 43 + } 44 + async set(key: string, val: NodeSavedSession) { 45 + const session = JSON.stringify(val); 46 + await this.db 47 + .insertInto("auth_session") 48 + .values({ key, session }) 49 + .onConflict((oc) => oc.doUpdateSet({ session })) 50 + .execute(); 51 + } 52 + async del(key: string) { 53 + await this.db.deleteFrom("auth_session").where("key", "=", key).execute(); 54 + } 47 55 } 48 -
+31
src/db.ts
··· 1 1 import SqliteDb from "better-sqlite3"; 2 2 import { randomBytes } from "crypto"; 3 + import e from "express"; 3 4 4 5 import { 5 6 Kysely, ··· 12 13 13 14 export type DatabaseSchema = { 14 15 paste: Paste; 16 + comment: Comment; 15 17 auth_state: AuthState; 16 18 auth_session: AuthSession; 17 19 }; ··· 19 21 export type Paste = { 20 22 uri: string; 21 23 authorDid: string; 24 + shortUrl: string; 22 25 code: string; 23 26 lang: string; 24 27 title: string; ··· 36 39 state: AuthStateJson; 37 40 }; 38 41 42 + export type Comment = { 43 + uri: string; 44 + authorDid: string; 45 + body: string; 46 + createdAt: string; 47 + indexedAt: string; 48 + pasteUri: string; 49 + pasteCid: string; 50 + }; 51 + 39 52 type AuthSessionJson = string; 40 53 type AuthStateJson = string; 41 54 ··· 83 96 await db.schema.dropTable("auth_state").execute(); 84 97 await db.schema.dropTable("auth_session").execute(); 85 98 await db.schema.dropTable("paste").execute(); 99 + }, 100 + }; 101 + 102 + migrations["002"] = { 103 + async up(db: Kysely<unknown>) { 104 + await db.schema 105 + .createTable("comment") 106 + .addColumn("uri", "varchar", (col) => col.primaryKey()) 107 + .addColumn("authorDid", "varchar", (col) => col.notNull()) 108 + .addColumn("body", "varchar", (col) => col.notNull()) 109 + .addColumn("createdAt", "varchar", (col) => col.notNull()) 110 + .addColumn("indexedAt", "varchar", (col) => col.notNull()) 111 + .addColumn("pasteUri", "varchar", (col) => col.notNull()) 112 + .addColumn("pasteCid", "varchar", (col) => col.notNull()) 113 + .execute(); 114 + }, 115 + async down(db: Kysely<unknown>) { 116 + await db.schema.dropTable("comments").execute(); 86 117 }, 87 118 }; 88 119
+30 -32
src/id-resolver.ts
··· 1 - import { IdResolver, MemoryCache } from '@atproto/identity' 2 - 3 - const HOUR = 60e3 * 60 4 - const DAY = HOUR * 24 1 + import { IdResolver, MemoryCache } from "@atproto/identity"; 5 2 3 + const HOUR = 60e3 * 60; 4 + const DAY = HOUR * 24; 6 5 7 6 export function createIdResolver() { 8 - return new IdResolver({ 9 - didCache: new MemoryCache(HOUR, DAY), 10 - }) 7 + return new IdResolver({ 8 + didCache: new MemoryCache(HOUR, DAY), 9 + }); 11 10 } 12 11 13 12 export interface BidirectionalResolver { 14 - resolveDidToHandle(did: string): Promise<string> 15 - resolveDidsToHandles(dids: string[]): Promise<Record<string, string>> 13 + resolveDidToHandle(did: string): Promise<string>; 14 + resolveDidsToHandles(dids: string[]): Promise<Record<string, string>>; 16 15 } 17 16 18 17 export function createBidirectionalResolver(resolver: IdResolver) { 19 - return { 20 - async resolveDidToHandle(did: string): Promise<string> { 21 - const didDoc = await resolver.did.resolveAtprotoData(did) 22 - const resolvedHandle = await resolver.handle.resolve(didDoc.handle) 23 - if (resolvedHandle === did) { 24 - return didDoc.handle 25 - } 26 - return did 27 - }, 18 + return { 19 + async resolveDidToHandle(did: string): Promise<string> { 20 + const didDoc = await resolver.did.resolveAtprotoData(did); 21 + const resolvedHandle = await resolver.handle.resolve(didDoc.handle); 22 + if (resolvedHandle === did) { 23 + return didDoc.handle; 24 + } 25 + return did; 26 + }, 28 27 29 - async resolveDidsToHandles( 30 - dids: string[] 31 - ): Promise<Record<string, string>> { 32 - const didHandleMap: Record<string, string> = {} 33 - const resolves = await Promise.all( 34 - dids.map((did) => this.resolveDidToHandle(did).catch((_) => did)) 35 - ) 36 - for (let i = 0; i < dids.length; i++) { 37 - didHandleMap[dids[i]] = resolves[i] 38 - } 39 - return didHandleMap 40 - }, 41 - } 28 + async resolveDidsToHandles( 29 + dids: string[], 30 + ): Promise<Record<string, string>> { 31 + const didHandleMap: Record<string, string> = {}; 32 + const resolves = await Promise.all( 33 + dids.map((did) => this.resolveDidToHandle(did).catch((_) => did)), 34 + ); 35 + for (let i = 0; i < dids.length; i++) { 36 + didHandleMap[dids[i]] = resolves[i]; 37 + } 38 + return didHandleMap; 39 + }, 40 + }; 42 41 } 43 -
+2 -2
src/index.ts
··· 32 32 ) {} 33 33 34 34 static async create() { 35 - const db: Database = createDb(env.DB_PATH); 35 + const db: Database = createDb(env.PLONK_DB_PATH); 36 36 await migrateToLatest(db); 37 37 const idResolver = createIdResolver(); 38 38 const ctx: Ctx = { ··· 55 55 app.use(router); 56 56 app.use((_req, res) => res.sendStatus(404)); 57 57 58 - const server = app.listen(env.PORT); 58 + const server = app.listen(env.PLONK_PORT); 59 59 60 60 return new Server(app, server, ctx); 61 61 }
+40 -8
src/ingester.ts
··· 2 2 import { IdResolver } from "@atproto/identity"; 3 3 import { Firehose } from "@atproto/sync"; 4 4 import type { Database } from "#/db"; 5 - import { newShortUrl } from "#/db"; 6 - import * as Paste from "#/lexicons/types/ovh/plonk/paste"; 5 + import * as Paste from "#/lexicons/types/li/plonk/paste"; 6 + import * as Comment from "#/lexicons/types/li/plonk/comment"; 7 7 8 8 export function createIngester(db: Database, idResolver: IdResolver) { 9 9 const logger = pino({ name: "firehose ingestion" }); ··· 17 17 18 18 // If the write is a valid status update 19 19 if ( 20 - evt.collection === "ovh.plonk.paste" && 20 + evt.collection === "li.plonk.paste" && 21 21 Paste.isRecord(record) && 22 22 Paste.validateRecord(record).success 23 23 ) { 24 - // Store the status in our SQLite 25 - const short_url = await newShortUrl(db); 26 24 await db 27 25 .insertInto("paste") 28 26 .values({ 29 27 uri: evt.uri.toString(), 30 - shortUrl, 28 + shortUrl: record.shortUrl, 31 29 authorDid: evt.did, 32 30 code: record.code, 33 31 lang: record.lang, ··· 44 42 }), 45 43 ) 46 44 .execute(); 45 + } else if ( 46 + evt.collection === "li.plonk.comment" && 47 + Comment.isRecord(record) && 48 + Comment.validateRecord(record).success 49 + ) { 50 + await db 51 + .insertInto("comment") 52 + .values({ 53 + uri: evt.uri.toString(), 54 + authorDid: evt.did, 55 + body: record.content, 56 + pasteUri: record.post.uri, 57 + pasteCid: record.post.cid, 58 + createdAt: record.createdAt, 59 + indexedAt: now.toISOString(), 60 + }) 61 + .onConflict((oc) => 62 + oc.column("uri").doUpdateSet({ 63 + body: record.content, 64 + pasteUri: record.post.uri, 65 + pasteCid: record.post.cid, 66 + indexedAt: now.toISOString(), 67 + }), 68 + ) 69 + .execute(); 47 70 } 48 71 } else if ( 49 72 evt.event === "delete" && 50 - evt.collection === "ovh.plonk.paste" 73 + evt.collection === "li.plonk.paste" 51 74 ) { 52 75 // Remove the status from our SQLite 53 76 await db 54 77 .deleteFrom("paste") 55 78 .where("uri", "=", evt.uri.toString()) 56 79 .execute(); 80 + } else if ( 81 + evt.event === "delete" && 82 + evt.collection === "li.plonk.comment" 83 + ) { 84 + // Remove the status from our SQLite 85 + await db 86 + .deleteFrom("comment") 87 + .where("uri", "=", evt.uri.toString()) 88 + .execute(); 57 89 } 58 90 }, 59 91 onError: (err) => { 60 92 logger.error({ err }, "error on firehose ingestion"); 61 93 }, 62 - filterCollections: ["ovh.plonk.paste"], 94 + filterCollections: ["li.plonk.paste"], 63 95 excludeIdentity: true, 64 96 excludeAccount: true, 65 97 });
+113 -89
src/lexicons/index.ts
··· 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 4 import { 5 - createServer as createXrpcServer, 6 - Server as XrpcServer, 7 - Options as XrpcOptions, 8 - AuthVerifier, 9 - StreamAuthVerifier, 10 - } from '@atproto/xrpc-server' 11 - import { schemas } from './lexicons' 5 + createServer as createXrpcServer, 6 + Server as XrpcServer, 7 + Options as XrpcOptions, 8 + AuthVerifier, 9 + StreamAuthVerifier, 10 + } from "@atproto/xrpc-server"; 11 + import { schemas } from "./lexicons"; 12 + import * as ComAtprotoRepoGetRecord from "./types/com/atproto/repo/getRecord"; 13 + import * as ComAtprotoRepoListRecords from "./types/com/atproto/repo/listRecords"; 12 14 13 15 export function createServer(options?: XrpcOptions): Server { 14 - return new Server(options) 16 + return new Server(options); 15 17 } 16 18 17 19 export class Server { 18 - xrpc: XrpcServer 19 - ovh: OvhNS 20 - app: AppNS 21 - com: ComNS 20 + xrpc: XrpcServer; 21 + li: LiNS; 22 + com: ComNS; 23 + app: AppNS; 22 24 23 - constructor(options?: XrpcOptions) { 24 - this.xrpc = createXrpcServer(schemas, options) 25 - this.ovh = new OvhNS(this) 26 - this.app = new AppNS(this) 27 - this.com = new ComNS(this) 28 - } 25 + constructor(options?: XrpcOptions) { 26 + this.xrpc = createXrpcServer(schemas, options); 27 + this.li = new LiNS(this); 28 + this.com = new ComNS(this); 29 + this.app = new AppNS(this); 30 + } 29 31 } 30 32 31 - export class OvhNS { 32 - _server: Server 33 - plonk: OvhPlonkNS 33 + export class LiNS { 34 + _server: Server; 35 + plonk: LiPlonkNS; 34 36 35 - constructor(server: Server) { 36 - this._server = server 37 - this.plonk = new OvhPlonkNS(server) 38 - } 37 + constructor(server: Server) { 38 + this._server = server; 39 + this.plonk = new LiPlonkNS(server); 40 + } 39 41 } 40 42 41 - export class OvhPlonkNS { 42 - _server: Server 43 + export class LiPlonkNS { 44 + _server: Server; 43 45 44 - constructor(server: Server) { 45 - this._server = server 46 - } 46 + constructor(server: Server) { 47 + this._server = server; 48 + } 47 49 } 48 50 49 - export class AppNS { 50 - _server: Server 51 - bsky: AppBskyNS 51 + export class ComNS { 52 + _server: Server; 53 + atproto: ComAtprotoNS; 52 54 53 - constructor(server: Server) { 54 - this._server = server 55 - this.bsky = new AppBskyNS(server) 56 - } 55 + constructor(server: Server) { 56 + this._server = server; 57 + this.atproto = new ComAtprotoNS(server); 58 + } 57 59 } 58 60 59 - export class AppBskyNS { 60 - _server: Server 61 - actor: AppBskyActorNS 61 + export class ComAtprotoNS { 62 + _server: Server; 63 + repo: ComAtprotoRepoNS; 62 64 63 - constructor(server: Server) { 64 - this._server = server 65 - this.actor = new AppBskyActorNS(server) 66 - } 65 + constructor(server: Server) { 66 + this._server = server; 67 + this.repo = new ComAtprotoRepoNS(server); 68 + } 67 69 } 68 70 69 - export class AppBskyActorNS { 70 - _server: Server 71 + export class ComAtprotoRepoNS { 72 + _server: Server; 71 73 72 - constructor(server: Server) { 73 - this._server = server 74 - } 74 + constructor(server: Server) { 75 + this._server = server; 76 + } 77 + 78 + getRecord<AV extends AuthVerifier>( 79 + cfg: ConfigOf< 80 + AV, 81 + ComAtprotoRepoGetRecord.Handler<ExtractAuth<AV>>, 82 + ComAtprotoRepoGetRecord.HandlerReqCtx<ExtractAuth<AV>> 83 + >, 84 + ) { 85 + const nsid = "com.atproto.repo.getRecord"; // @ts-ignore 86 + return this._server.xrpc.method(nsid, cfg); 87 + } 88 + 89 + listRecords<AV extends AuthVerifier>( 90 + cfg: ConfigOf< 91 + AV, 92 + ComAtprotoRepoListRecords.Handler<ExtractAuth<AV>>, 93 + ComAtprotoRepoListRecords.HandlerReqCtx<ExtractAuth<AV>> 94 + >, 95 + ) { 96 + const nsid = "com.atproto.repo.listRecords"; // @ts-ignore 97 + return this._server.xrpc.method(nsid, cfg); 98 + } 75 99 } 76 100 77 - export class ComNS { 78 - _server: Server 79 - atproto: ComAtprotoNS 101 + export class AppNS { 102 + _server: Server; 103 + bsky: AppBskyNS; 80 104 81 - constructor(server: Server) { 82 - this._server = server 83 - this.atproto = new ComAtprotoNS(server) 84 - } 105 + constructor(server: Server) { 106 + this._server = server; 107 + this.bsky = new AppBskyNS(server); 108 + } 85 109 } 86 110 87 - export class ComAtprotoNS { 88 - _server: Server 89 - repo: ComAtprotoRepoNS 111 + export class AppBskyNS { 112 + _server: Server; 113 + actor: AppBskyActorNS; 90 114 91 - constructor(server: Server) { 92 - this._server = server 93 - this.repo = new ComAtprotoRepoNS(server) 94 - } 115 + constructor(server: Server) { 116 + this._server = server; 117 + this.actor = new AppBskyActorNS(server); 118 + } 95 119 } 96 120 97 - export class ComAtprotoRepoNS { 98 - _server: Server 121 + export class AppBskyActorNS { 122 + _server: Server; 99 123 100 - constructor(server: Server) { 101 - this._server = server 102 - } 124 + constructor(server: Server) { 125 + this._server = server; 126 + } 103 127 } 104 128 105 129 type SharedRateLimitOpts<T> = { 106 - name: string 107 - calcKey?: (ctx: T) => string 108 - calcPoints?: (ctx: T) => number 109 - } 130 + name: string; 131 + calcKey?: (ctx: T) => string; 132 + calcPoints?: (ctx: T) => number; 133 + }; 110 134 type RouteRateLimitOpts<T> = { 111 - durationMs: number 112 - points: number 113 - calcKey?: (ctx: T) => string 114 - calcPoints?: (ctx: T) => number 115 - } 116 - type HandlerOpts = { blobLimit?: number } 117 - type HandlerRateLimitOpts<T> = SharedRateLimitOpts<T> | RouteRateLimitOpts<T> 135 + durationMs: number; 136 + points: number; 137 + calcKey?: (ctx: T) => string; 138 + calcPoints?: (ctx: T) => number; 139 + }; 140 + type HandlerOpts = { blobLimit?: number }; 141 + type HandlerRateLimitOpts<T> = SharedRateLimitOpts<T> | RouteRateLimitOpts<T>; 118 142 type ConfigOf<Auth, Handler, ReqCtx> = 119 - | Handler 120 - | { 121 - auth?: Auth 122 - opts?: HandlerOpts 123 - rateLimit?: HandlerRateLimitOpts<ReqCtx> | HandlerRateLimitOpts<ReqCtx>[] 124 - handler: Handler 125 - } 143 + | Handler 144 + | { 145 + auth?: Auth; 146 + opts?: HandlerOpts; 147 + rateLimit?: HandlerRateLimitOpts<ReqCtx> | HandlerRateLimitOpts<ReqCtx>[]; 148 + handler: Handler; 149 + }; 126 150 type ExtractAuth<AV extends AuthVerifier | StreamAuthVerifier> = Extract< 127 - Awaited<ReturnType<AV>>, 128 - { credentials: unknown } 129 - > 151 + Awaited<ReturnType<AV>>, 152 + { credentials: unknown } 153 + >;
+491 -302
src/lexicons/lexicons.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { LexiconDoc, Lexicons } from '@atproto/lexicon' 4 + import { LexiconDoc, Lexicons } from "@atproto/lexicon"; 5 5 6 6 export const schemaDict = { 7 - ComAtprotoLabelDefs: { 8 - lexicon: 1, 9 - id: 'com.atproto.label.defs', 10 - defs: { 11 - label: { 12 - type: 'object', 13 - description: 14 - 'Metadata tag on an atproto resource (eg, repo or record).', 15 - required: ['src', 'uri', 'val', 'cts'], 16 - properties: { 17 - ver: { 18 - type: 'integer', 19 - description: 'The AT Protocol version of the label object.', 20 - }, 21 - src: { 22 - type: 'string', 23 - format: 'did', 24 - description: 'DID of the actor who created this label.', 25 - }, 26 - uri: { 27 - type: 'string', 28 - format: 'uri', 29 - description: 30 - 'AT URI of the record, repository (account), or other resource that this label applies to.', 31 - }, 32 - cid: { 33 - type: 'string', 34 - format: 'cid', 35 - description: 36 - "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", 37 - }, 38 - val: { 39 - type: 'string', 40 - maxLength: 128, 41 - description: 42 - 'The short string name of the value or type of this label.', 43 - }, 44 - neg: { 45 - type: 'boolean', 46 - description: 47 - 'If true, this is a negation label, overwriting a previous label.', 48 - }, 49 - cts: { 50 - type: 'string', 51 - format: 'datetime', 52 - description: 'Timestamp when this label was created.', 53 - }, 54 - exp: { 55 - type: 'string', 56 - format: 'datetime', 57 - description: 58 - 'Timestamp at which this label expires (no longer applies).', 59 - }, 60 - sig: { 61 - type: 'bytes', 62 - description: 'Signature of dag-cbor encoded label.', 63 - }, 64 - }, 65 - }, 66 - selfLabels: { 67 - type: 'object', 68 - description: 69 - 'Metadata tags on an atproto record, published by the author within the record.', 70 - required: ['values'], 71 - properties: { 72 - values: { 73 - type: 'array', 74 - items: { 75 - type: 'ref', 76 - ref: 'lex:com.atproto.label.defs#selfLabel', 77 - }, 78 - maxLength: 10, 79 - }, 80 - }, 81 - }, 82 - selfLabel: { 83 - type: 'object', 84 - description: 85 - 'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.', 86 - required: ['val'], 87 - properties: { 88 - val: { 89 - type: 'string', 90 - maxLength: 128, 91 - description: 92 - 'The short string name of the value or type of this label.', 93 - }, 94 - }, 95 - }, 96 - labelValueDefinition: { 97 - type: 'object', 98 - description: 99 - 'Declares a label value and its expected interpretations and behaviors.', 100 - required: ['identifier', 'severity', 'blurs', 'locales'], 101 - properties: { 102 - identifier: { 103 - type: 'string', 104 - description: 105 - "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 106 - maxLength: 100, 107 - maxGraphemes: 100, 108 - }, 109 - severity: { 110 - type: 'string', 111 - description: 112 - "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 113 - knownValues: ['inform', 'alert', 'none'], 114 - }, 115 - blurs: { 116 - type: 'string', 117 - description: 118 - "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 119 - knownValues: ['content', 'media', 'none'], 120 - }, 121 - defaultSetting: { 122 - type: 'string', 123 - description: 'The default setting for this label.', 124 - knownValues: ['ignore', 'warn', 'hide'], 125 - default: 'warn', 126 - }, 127 - adultOnly: { 128 - type: 'boolean', 129 - description: 130 - 'Does the user need to have adult content enabled in order to configure this label?', 131 - }, 132 - locales: { 133 - type: 'array', 134 - items: { 135 - type: 'ref', 136 - ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings', 137 - }, 138 - }, 139 - }, 140 - }, 141 - labelValueDefinitionStrings: { 142 - type: 'object', 143 - description: 144 - 'Strings which describe the label in the UI, localized into a specific language.', 145 - required: ['lang', 'name', 'description'], 146 - properties: { 147 - lang: { 148 - type: 'string', 149 - description: 150 - 'The code of the language these strings are written in.', 151 - format: 'language', 152 - }, 153 - name: { 154 - type: 'string', 155 - description: 'A short human-readable name for the label.', 156 - maxGraphemes: 64, 157 - maxLength: 640, 158 - }, 159 - description: { 160 - type: 'string', 161 - description: 162 - 'A longer description of what the label means and why it might be applied.', 163 - maxGraphemes: 10000, 164 - maxLength: 100000, 165 - }, 166 - }, 167 - }, 168 - labelValue: { 169 - type: 'string', 170 - knownValues: [ 171 - '!hide', 172 - '!no-promote', 173 - '!warn', 174 - '!no-unauthenticated', 175 - 'dmca-violation', 176 - 'doxxing', 177 - 'porn', 178 - 'sexual', 179 - 'nudity', 180 - 'nsfl', 181 - 'gore', 182 - ], 183 - }, 184 - }, 185 - }, 186 - OvhPlonkPaste: { 187 - lexicon: 1, 188 - id: 'ovh.plonk.paste', 189 - defs: { 190 - main: { 191 - type: 'record', 192 - key: 'tid', 193 - record: { 194 - type: 'object', 195 - required: ['code', 'lang', 'title', 'createdAt'], 196 - properties: { 197 - code: { 198 - type: 'string', 199 - minLength: 1, 200 - maxGraphemes: 65536, 201 - maxLength: 65536, 202 - }, 203 - lang: { 204 - type: 'string', 205 - minLength: 1, 206 - maxGraphemes: 20, 207 - maxLength: 20, 208 - }, 209 - title: { 210 - type: 'string', 211 - minLength: 1, 212 - maxGraphemes: 100, 213 - maxLength: 100, 214 - }, 215 - createdAt: { 216 - type: 'string', 217 - format: 'datetime', 218 - }, 219 - }, 220 - }, 221 - }, 222 - }, 223 - }, 224 - AppBskyActorProfile: { 225 - lexicon: 1, 226 - id: 'app.bsky.actor.profile', 227 - defs: { 228 - main: { 229 - type: 'record', 230 - description: 'A declaration of a Bluesky account profile.', 231 - key: 'literal:self', 232 - record: { 233 - type: 'object', 234 - properties: { 235 - displayName: { 236 - type: 'string', 237 - maxGraphemes: 64, 238 - maxLength: 640, 239 - }, 240 - description: { 241 - type: 'string', 242 - description: 'Free-form profile description text.', 243 - maxGraphemes: 256, 244 - maxLength: 2560, 245 - }, 246 - avatar: { 247 - type: 'blob', 248 - description: 249 - "Small image to be displayed next to posts from account. AKA, 'profile picture'", 250 - accept: ['image/png', 'image/jpeg'], 251 - maxSize: 1000000, 252 - }, 253 - banner: { 254 - type: 'blob', 255 - description: 256 - 'Larger horizontal image to display behind profile view.', 257 - accept: ['image/png', 'image/jpeg'], 258 - maxSize: 1000000, 259 - }, 260 - labels: { 261 - type: 'union', 262 - description: 263 - 'Self-label values, specific to the Bluesky application, on the overall account.', 264 - refs: ['lex:com.atproto.label.defs#selfLabels'], 265 - }, 266 - joinedViaStarterPack: { 267 - type: 'ref', 268 - ref: 'lex:com.atproto.repo.strongRef', 269 - }, 270 - createdAt: { 271 - type: 'string', 272 - format: 'datetime', 273 - }, 274 - }, 275 - }, 276 - }, 277 - }, 278 - }, 279 - ComAtprotoRepoStrongRef: { 280 - lexicon: 1, 281 - id: 'com.atproto.repo.strongRef', 282 - description: 'A URI with a content-hash fingerprint.', 283 - defs: { 284 - main: { 285 - type: 'object', 286 - required: ['uri', 'cid'], 287 - properties: { 288 - uri: { 289 - type: 'string', 290 - format: 'at-uri', 291 - }, 292 - cid: { 293 - type: 'string', 294 - format: 'cid', 295 - }, 296 - }, 297 - }, 298 - }, 299 - }, 300 - } 301 - export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[] 302 - export const lexicons: Lexicons = new Lexicons(schemas) 7 + LiPlonkComment: { 8 + lexicon: 1, 9 + id: "li.plonk.comment", 10 + defs: { 11 + main: { 12 + type: "record", 13 + key: "tid", 14 + record: { 15 + type: "object", 16 + required: ["content", "createdAt", "post"], 17 + properties: { 18 + content: { 19 + type: "string", 20 + maxLength: 100000, 21 + maxGraphemes: 10000, 22 + description: "comment body", 23 + }, 24 + createdAt: { 25 + type: "string", 26 + format: "datetime", 27 + description: "comment creation timestamp", 28 + }, 29 + post: { 30 + type: "ref", 31 + ref: "lex:com.atproto.repo.strongRef", 32 + }, 33 + }, 34 + }, 35 + }, 36 + }, 37 + }, 38 + ComAtprotoLabelDefs: { 39 + lexicon: 1, 40 + id: "com.atproto.label.defs", 41 + defs: { 42 + label: { 43 + type: "object", 44 + description: 45 + "Metadata tag on an atproto resource (eg, repo or record).", 46 + required: ["src", "uri", "val", "cts"], 47 + properties: { 48 + ver: { 49 + type: "integer", 50 + description: "The AT Protocol version of the label object.", 51 + }, 52 + src: { 53 + type: "string", 54 + format: "did", 55 + description: "DID of the actor who created this label.", 56 + }, 57 + uri: { 58 + type: "string", 59 + format: "uri", 60 + description: 61 + "AT URI of the record, repository (account), or other resource that this label applies to.", 62 + }, 63 + cid: { 64 + type: "string", 65 + format: "cid", 66 + description: 67 + "Optionally, CID specifying the specific version of 'uri' resource this label applies to.", 68 + }, 69 + val: { 70 + type: "string", 71 + maxLength: 128, 72 + description: 73 + "The short string name of the value or type of this label.", 74 + }, 75 + neg: { 76 + type: "boolean", 77 + description: 78 + "If true, this is a negation label, overwriting a previous label.", 79 + }, 80 + cts: { 81 + type: "string", 82 + format: "datetime", 83 + description: "Timestamp when this label was created.", 84 + }, 85 + exp: { 86 + type: "string", 87 + format: "datetime", 88 + description: 89 + "Timestamp at which this label expires (no longer applies).", 90 + }, 91 + sig: { 92 + type: "bytes", 93 + description: "Signature of dag-cbor encoded label.", 94 + }, 95 + }, 96 + }, 97 + selfLabels: { 98 + type: "object", 99 + description: 100 + "Metadata tags on an atproto record, published by the author within the record.", 101 + required: ["values"], 102 + properties: { 103 + values: { 104 + type: "array", 105 + items: { 106 + type: "ref", 107 + ref: "lex:com.atproto.label.defs#selfLabel", 108 + }, 109 + maxLength: 10, 110 + }, 111 + }, 112 + }, 113 + selfLabel: { 114 + type: "object", 115 + description: 116 + "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", 117 + required: ["val"], 118 + properties: { 119 + val: { 120 + type: "string", 121 + maxLength: 128, 122 + description: 123 + "The short string name of the value or type of this label.", 124 + }, 125 + }, 126 + }, 127 + labelValueDefinition: { 128 + type: "object", 129 + description: 130 + "Declares a label value and its expected interpretations and behaviors.", 131 + required: ["identifier", "severity", "blurs", "locales"], 132 + properties: { 133 + identifier: { 134 + type: "string", 135 + description: 136 + "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 137 + maxLength: 100, 138 + maxGraphemes: 100, 139 + }, 140 + severity: { 141 + type: "string", 142 + description: 143 + "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 144 + knownValues: ["inform", "alert", "none"], 145 + }, 146 + blurs: { 147 + type: "string", 148 + description: 149 + "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 150 + knownValues: ["content", "media", "none"], 151 + }, 152 + defaultSetting: { 153 + type: "string", 154 + description: "The default setting for this label.", 155 + knownValues: ["ignore", "warn", "hide"], 156 + default: "warn", 157 + }, 158 + adultOnly: { 159 + type: "boolean", 160 + description: 161 + "Does the user need to have adult content enabled in order to configure this label?", 162 + }, 163 + locales: { 164 + type: "array", 165 + items: { 166 + type: "ref", 167 + ref: "lex:com.atproto.label.defs#labelValueDefinitionStrings", 168 + }, 169 + }, 170 + }, 171 + }, 172 + labelValueDefinitionStrings: { 173 + type: "object", 174 + description: 175 + "Strings which describe the label in the UI, localized into a specific language.", 176 + required: ["lang", "name", "description"], 177 + properties: { 178 + lang: { 179 + type: "string", 180 + description: 181 + "The code of the language these strings are written in.", 182 + format: "language", 183 + }, 184 + name: { 185 + type: "string", 186 + description: "A short human-readable name for the label.", 187 + maxGraphemes: 64, 188 + maxLength: 640, 189 + }, 190 + description: { 191 + type: "string", 192 + description: 193 + "A longer description of what the label means and why it might be applied.", 194 + maxGraphemes: 10000, 195 + maxLength: 100000, 196 + }, 197 + }, 198 + }, 199 + labelValue: { 200 + type: "string", 201 + knownValues: [ 202 + "!hide", 203 + "!no-promote", 204 + "!warn", 205 + "!no-unauthenticated", 206 + "dmca-violation", 207 + "doxxing", 208 + "porn", 209 + "sexual", 210 + "nudity", 211 + "nsfl", 212 + "gore", 213 + ], 214 + }, 215 + }, 216 + }, 217 + ComAtprotoRepoGetRecord: { 218 + lexicon: 1, 219 + id: "com.atproto.repo.getRecord", 220 + defs: { 221 + main: { 222 + type: "query", 223 + description: 224 + "Get a single record from a repository. Does not require auth.", 225 + parameters: { 226 + type: "params", 227 + required: ["repo", "collection", "rkey"], 228 + properties: { 229 + repo: { 230 + type: "string", 231 + format: "at-identifier", 232 + description: "The handle or DID of the repo.", 233 + }, 234 + collection: { 235 + type: "string", 236 + format: "nsid", 237 + description: "The NSID of the record collection.", 238 + }, 239 + rkey: { 240 + type: "string", 241 + description: "The Record Key.", 242 + }, 243 + cid: { 244 + type: "string", 245 + format: "cid", 246 + description: 247 + "The CID of the version of the record. If not specified, then return the most recent version.", 248 + }, 249 + }, 250 + }, 251 + output: { 252 + encoding: "application/json", 253 + schema: { 254 + type: "object", 255 + required: ["uri", "value"], 256 + properties: { 257 + uri: { 258 + type: "string", 259 + format: "at-uri", 260 + }, 261 + cid: { 262 + type: "string", 263 + format: "cid", 264 + }, 265 + value: { 266 + type: "unknown", 267 + }, 268 + }, 269 + }, 270 + }, 271 + errors: [ 272 + { 273 + name: "RecordNotFound", 274 + }, 275 + ], 276 + }, 277 + }, 278 + }, 279 + ComAtprotoRepoListRecords: { 280 + lexicon: 1, 281 + id: "com.atproto.repo.listRecords", 282 + defs: { 283 + main: { 284 + type: "query", 285 + description: 286 + "List a range of records in a repository, matching a specific collection. Does not require auth.", 287 + parameters: { 288 + type: "params", 289 + required: ["repo", "collection"], 290 + properties: { 291 + repo: { 292 + type: "string", 293 + format: "at-identifier", 294 + description: "The handle or DID of the repo.", 295 + }, 296 + collection: { 297 + type: "string", 298 + format: "nsid", 299 + description: "The NSID of the record type.", 300 + }, 301 + limit: { 302 + type: "integer", 303 + minimum: 1, 304 + maximum: 100, 305 + default: 50, 306 + description: "The number of records to return.", 307 + }, 308 + cursor: { 309 + type: "string", 310 + }, 311 + rkeyStart: { 312 + type: "string", 313 + description: 314 + "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)", 315 + }, 316 + rkeyEnd: { 317 + type: "string", 318 + description: 319 + "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)", 320 + }, 321 + reverse: { 322 + type: "boolean", 323 + description: "Flag to reverse the order of the returned records.", 324 + }, 325 + }, 326 + }, 327 + output: { 328 + encoding: "application/json", 329 + schema: { 330 + type: "object", 331 + required: ["records"], 332 + properties: { 333 + cursor: { 334 + type: "string", 335 + }, 336 + records: { 337 + type: "array", 338 + items: { 339 + type: "ref", 340 + ref: "lex:com.atproto.repo.listRecords#record", 341 + }, 342 + }, 343 + }, 344 + }, 345 + }, 346 + }, 347 + record: { 348 + type: "object", 349 + required: ["uri", "cid", "value"], 350 + properties: { 351 + uri: { 352 + type: "string", 353 + format: "at-uri", 354 + }, 355 + cid: { 356 + type: "string", 357 + format: "cid", 358 + }, 359 + value: { 360 + type: "unknown", 361 + }, 362 + }, 363 + }, 364 + }, 365 + }, 366 + LiPlonkPaste: { 367 + lexicon: 1, 368 + id: "li.plonk.paste", 369 + defs: { 370 + main: { 371 + type: "record", 372 + key: "tid", 373 + record: { 374 + type: "object", 375 + required: ["code", "shortUrl", "lang", "title", "createdAt"], 376 + properties: { 377 + code: { 378 + type: "string", 379 + minLength: 1, 380 + maxGraphemes: 65536, 381 + maxLength: 65536, 382 + }, 383 + shortUrl: { 384 + type: "string", 385 + minLength: 2, 386 + maxGraphemes: 10, 387 + maxLength: 10, 388 + }, 389 + lang: { 390 + type: "string", 391 + minLength: 1, 392 + maxGraphemes: 20, 393 + maxLength: 20, 394 + }, 395 + title: { 396 + type: "string", 397 + minLength: 1, 398 + maxGraphemes: 100, 399 + maxLength: 100, 400 + }, 401 + createdAt: { 402 + type: "string", 403 + format: "datetime", 404 + }, 405 + }, 406 + }, 407 + }, 408 + }, 409 + }, 410 + AppBskyActorProfile: { 411 + lexicon: 1, 412 + id: "app.bsky.actor.profile", 413 + defs: { 414 + main: { 415 + type: "record", 416 + description: "A declaration of a Bluesky account profile.", 417 + key: "literal:self", 418 + record: { 419 + type: "object", 420 + properties: { 421 + displayName: { 422 + type: "string", 423 + maxGraphemes: 64, 424 + maxLength: 640, 425 + }, 426 + description: { 427 + type: "string", 428 + description: "Free-form profile description text.", 429 + maxGraphemes: 256, 430 + maxLength: 2560, 431 + }, 432 + avatar: { 433 + type: "blob", 434 + description: 435 + "Small image to be displayed next to posts from account. AKA, 'profile picture'", 436 + accept: ["image/png", "image/jpeg"], 437 + maxSize: 1000000, 438 + }, 439 + banner: { 440 + type: "blob", 441 + description: 442 + "Larger horizontal image to display behind profile view.", 443 + accept: ["image/png", "image/jpeg"], 444 + maxSize: 1000000, 445 + }, 446 + labels: { 447 + type: "union", 448 + description: 449 + "Self-label values, specific to the Bluesky application, on the overall account.", 450 + refs: ["lex:com.atproto.label.defs#selfLabels"], 451 + }, 452 + joinedViaStarterPack: { 453 + type: "ref", 454 + ref: "lex:com.atproto.repo.strongRef", 455 + }, 456 + createdAt: { 457 + type: "string", 458 + format: "datetime", 459 + }, 460 + }, 461 + }, 462 + }, 463 + }, 464 + }, 465 + ComAtprotoRepoStrongRef: { 466 + lexicon: 1, 467 + id: "com.atproto.repo.strongRef", 468 + description: "A URI with a content-hash fingerprint.", 469 + defs: { 470 + main: { 471 + type: "object", 472 + required: ["uri", "cid"], 473 + properties: { 474 + uri: { 475 + type: "string", 476 + format: "at-uri", 477 + }, 478 + cid: { 479 + type: "string", 480 + format: "cid", 481 + }, 482 + }, 483 + }, 484 + }, 485 + }, 486 + }; 487 + export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]; 488 + export const lexicons: Lexicons = new Lexicons(schemas); 303 489 export const ids = { 304 - ComAtprotoLabelDefs: 'com.atproto.label.defs', 305 - OvhPlonkPaste: 'ovh.plonk.paste', 306 - AppBskyActorProfile: 'app.bsky.actor.profile', 307 - ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef', 308 - } 490 + LiPlonkComment: "li.plonk.comment", 491 + ComAtprotoLabelDefs: "com.atproto.label.defs", 492 + ComAtprotoRepoGetRecord: "com.atproto.repo.getRecord", 493 + ComAtprotoRepoListRecords: "com.atproto.repo.listRecords", 494 + LiPlonkPaste: "li.plonk.paste", 495 + AppBskyActorProfile: "app.bsky.actor.profile", 496 + ComAtprotoRepoStrongRef: "com.atproto.repo.strongRef", 497 + };
+26 -26
src/lexicons/types/app/bsky/actor/profile.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { lexicons } from '../../../../lexicons' 6 - import { isObj, hasProp } from '../../../../util' 7 - import { CID } from 'multiformats/cid' 8 - import * as ComAtprotoLabelDefs from '../../../com/atproto/label/defs' 9 - import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' 4 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import * as ComAtprotoLabelDefs from "../../../com/atproto/label/defs"; 9 + import * as ComAtprotoRepoStrongRef from "../../../com/atproto/repo/strongRef"; 10 10 11 11 export interface Record { 12 - displayName?: string 13 - /** Free-form profile description text. */ 14 - description?: string 15 - /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 16 - avatar?: BlobRef 17 - /** Larger horizontal image to display behind profile view. */ 18 - banner?: BlobRef 19 - labels?: 20 - | ComAtprotoLabelDefs.SelfLabels 21 - | { $type: string; [k: string]: unknown } 22 - joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main 23 - createdAt?: string 24 - [k: string]: unknown 12 + displayName?: string; 13 + /** Free-form profile description text. */ 14 + description?: string; 15 + /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 16 + avatar?: BlobRef; 17 + /** Larger horizontal image to display behind profile view. */ 18 + banner?: BlobRef; 19 + labels?: 20 + | ComAtprotoLabelDefs.SelfLabels 21 + | { $type: string; [k: string]: unknown }; 22 + joinedViaStarterPack?: ComAtprotoRepoStrongRef.Main; 23 + createdAt?: string; 24 + [k: string]: unknown; 25 25 } 26 26 27 27 export function isRecord(v: unknown): v is Record { 28 - return ( 29 - isObj(v) && 30 - hasProp(v, '$type') && 31 - (v.$type === 'app.bsky.actor.profile#main' || 32 - v.$type === 'app.bsky.actor.profile') 33 - ) 28 + return ( 29 + isObj(v) && 30 + hasProp(v, "$type") && 31 + (v.$type === "app.bsky.actor.profile#main" || 32 + v.$type === "app.bsky.actor.profile") 33 + ); 34 34 } 35 35 36 36 export function validateRecord(v: unknown): ValidationResult { 37 - return lexicons.validate('app.bsky.actor.profile#main', v) 37 + return lexicons.validate("app.bsky.actor.profile#main", v); 38 38 }
+94 -94
src/lexicons/types/com/atproto/label/defs.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { lexicons } from '../../../../lexicons' 6 - import { isObj, hasProp } from '../../../../util' 7 - import { CID } from 'multiformats/cid' 4 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 8 9 9 /** Metadata tag on an atproto resource (eg, repo or record). */ 10 10 export interface Label { 11 - /** The AT Protocol version of the label object. */ 12 - ver?: number 13 - /** DID of the actor who created this label. */ 14 - src: string 15 - /** AT URI of the record, repository (account), or other resource that this label applies to. */ 16 - uri: string 17 - /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ 18 - cid?: string 19 - /** The short string name of the value or type of this label. */ 20 - val: string 21 - /** If true, this is a negation label, overwriting a previous label. */ 22 - neg?: boolean 23 - /** Timestamp when this label was created. */ 24 - cts: string 25 - /** Timestamp at which this label expires (no longer applies). */ 26 - exp?: string 27 - /** Signature of dag-cbor encoded label. */ 28 - sig?: Uint8Array 29 - [k: string]: unknown 11 + /** The AT Protocol version of the label object. */ 12 + ver?: number; 13 + /** DID of the actor who created this label. */ 14 + src: string; 15 + /** AT URI of the record, repository (account), or other resource that this label applies to. */ 16 + uri: string; 17 + /** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */ 18 + cid?: string; 19 + /** The short string name of the value or type of this label. */ 20 + val: string; 21 + /** If true, this is a negation label, overwriting a previous label. */ 22 + neg?: boolean; 23 + /** Timestamp when this label was created. */ 24 + cts: string; 25 + /** Timestamp at which this label expires (no longer applies). */ 26 + exp?: string; 27 + /** Signature of dag-cbor encoded label. */ 28 + sig?: Uint8Array; 29 + [k: string]: unknown; 30 30 } 31 31 32 32 export function isLabel(v: unknown): v is Label { 33 - return ( 34 - isObj(v) && 35 - hasProp(v, '$type') && 36 - v.$type === 'com.atproto.label.defs#label' 37 - ) 33 + return ( 34 + isObj(v) && 35 + hasProp(v, "$type") && 36 + v.$type === "com.atproto.label.defs#label" 37 + ); 38 38 } 39 39 40 40 export function validateLabel(v: unknown): ValidationResult { 41 - return lexicons.validate('com.atproto.label.defs#label', v) 41 + return lexicons.validate("com.atproto.label.defs#label", v); 42 42 } 43 43 44 44 /** Metadata tags on an atproto record, published by the author within the record. */ 45 45 export interface SelfLabels { 46 - values: SelfLabel[] 47 - [k: string]: unknown 46 + values: SelfLabel[]; 47 + [k: string]: unknown; 48 48 } 49 49 50 50 export function isSelfLabels(v: unknown): v is SelfLabels { 51 - return ( 52 - isObj(v) && 53 - hasProp(v, '$type') && 54 - v.$type === 'com.atproto.label.defs#selfLabels' 55 - ) 51 + return ( 52 + isObj(v) && 53 + hasProp(v, "$type") && 54 + v.$type === "com.atproto.label.defs#selfLabels" 55 + ); 56 56 } 57 57 58 58 export function validateSelfLabels(v: unknown): ValidationResult { 59 - return lexicons.validate('com.atproto.label.defs#selfLabels', v) 59 + return lexicons.validate("com.atproto.label.defs#selfLabels", v); 60 60 } 61 61 62 62 /** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */ 63 63 export interface SelfLabel { 64 - /** The short string name of the value or type of this label. */ 65 - val: string 66 - [k: string]: unknown 64 + /** The short string name of the value or type of this label. */ 65 + val: string; 66 + [k: string]: unknown; 67 67 } 68 68 69 69 export function isSelfLabel(v: unknown): v is SelfLabel { 70 - return ( 71 - isObj(v) && 72 - hasProp(v, '$type') && 73 - v.$type === 'com.atproto.label.defs#selfLabel' 74 - ) 70 + return ( 71 + isObj(v) && 72 + hasProp(v, "$type") && 73 + v.$type === "com.atproto.label.defs#selfLabel" 74 + ); 75 75 } 76 76 77 77 export function validateSelfLabel(v: unknown): ValidationResult { 78 - return lexicons.validate('com.atproto.label.defs#selfLabel', v) 78 + return lexicons.validate("com.atproto.label.defs#selfLabel", v); 79 79 } 80 80 81 81 /** Declares a label value and its expected interpretations and behaviors. */ 82 82 export interface LabelValueDefinition { 83 - /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 84 - identifier: string 85 - /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */ 86 - severity: 'inform' | 'alert' | 'none' | (string & {}) 87 - /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */ 88 - blurs: 'content' | 'media' | 'none' | (string & {}) 89 - /** The default setting for this label. */ 90 - defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {}) 91 - /** Does the user need to have adult content enabled in order to configure this label? */ 92 - adultOnly?: boolean 93 - locales: LabelValueDefinitionStrings[] 94 - [k: string]: unknown 83 + /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 84 + identifier: string; 85 + /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */ 86 + severity: "inform" | "alert" | "none" | (string & {}); 87 + /** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */ 88 + blurs: "content" | "media" | "none" | (string & {}); 89 + /** The default setting for this label. */ 90 + defaultSetting: "ignore" | "warn" | "hide" | (string & {}); 91 + /** Does the user need to have adult content enabled in order to configure this label? */ 92 + adultOnly?: boolean; 93 + locales: LabelValueDefinitionStrings[]; 94 + [k: string]: unknown; 95 95 } 96 96 97 97 export function isLabelValueDefinition(v: unknown): v is LabelValueDefinition { 98 - return ( 99 - isObj(v) && 100 - hasProp(v, '$type') && 101 - v.$type === 'com.atproto.label.defs#labelValueDefinition' 102 - ) 98 + return ( 99 + isObj(v) && 100 + hasProp(v, "$type") && 101 + v.$type === "com.atproto.label.defs#labelValueDefinition" 102 + ); 103 103 } 104 104 105 105 export function validateLabelValueDefinition(v: unknown): ValidationResult { 106 - return lexicons.validate('com.atproto.label.defs#labelValueDefinition', v) 106 + return lexicons.validate("com.atproto.label.defs#labelValueDefinition", v); 107 107 } 108 108 109 109 /** Strings which describe the label in the UI, localized into a specific language. */ 110 110 export interface LabelValueDefinitionStrings { 111 - /** The code of the language these strings are written in. */ 112 - lang: string 113 - /** A short human-readable name for the label. */ 114 - name: string 115 - /** A longer description of what the label means and why it might be applied. */ 116 - description: string 117 - [k: string]: unknown 111 + /** The code of the language these strings are written in. */ 112 + lang: string; 113 + /** A short human-readable name for the label. */ 114 + name: string; 115 + /** A longer description of what the label means and why it might be applied. */ 116 + description: string; 117 + [k: string]: unknown; 118 118 } 119 119 120 120 export function isLabelValueDefinitionStrings( 121 - v: unknown, 121 + v: unknown, 122 122 ): v is LabelValueDefinitionStrings { 123 - return ( 124 - isObj(v) && 125 - hasProp(v, '$type') && 126 - v.$type === 'com.atproto.label.defs#labelValueDefinitionStrings' 127 - ) 123 + return ( 124 + isObj(v) && 125 + hasProp(v, "$type") && 126 + v.$type === "com.atproto.label.defs#labelValueDefinitionStrings" 127 + ); 128 128 } 129 129 130 130 export function validateLabelValueDefinitionStrings( 131 - v: unknown, 131 + v: unknown, 132 132 ): ValidationResult { 133 - return lexicons.validate( 134 - 'com.atproto.label.defs#labelValueDefinitionStrings', 135 - v, 136 - ) 133 + return lexicons.validate( 134 + "com.atproto.label.defs#labelValueDefinitionStrings", 135 + v, 136 + ); 137 137 } 138 138 139 139 export type LabelValue = 140 - | '!hide' 141 - | '!no-promote' 142 - | '!warn' 143 - | '!no-unauthenticated' 144 - | 'dmca-violation' 145 - | 'doxxing' 146 - | 'porn' 147 - | 'sexual' 148 - | 'nudity' 149 - | 'nsfl' 150 - | 'gore' 151 - | (string & {}) 140 + | "!hide" 141 + | "!no-promote" 142 + | "!warn" 143 + | "!no-unauthenticated" 144 + | "dmca-violation" 145 + | "doxxing" 146 + | "porn" 147 + | "sexual" 148 + | "nudity" 149 + | "nsfl" 150 + | "gore" 151 + | (string & {});
+55
src/lexicons/types/com/atproto/repo/getRecord.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + 11 + export interface QueryParams { 12 + /** The handle or DID of the repo. */ 13 + repo: string; 14 + /** The NSID of the record collection. */ 15 + collection: string; 16 + /** The Record Key. */ 17 + rkey: string; 18 + /** The CID of the version of the record. If not specified, then return the most recent version. */ 19 + cid?: string; 20 + } 21 + 22 + export type InputSchema = undefined; 23 + 24 + export interface OutputSchema { 25 + uri: string; 26 + cid?: string; 27 + value: {}; 28 + [k: string]: unknown; 29 + } 30 + 31 + export type HandlerInput = undefined; 32 + 33 + export interface HandlerSuccess { 34 + encoding: "application/json"; 35 + body: OutputSchema; 36 + headers?: { [key: string]: string }; 37 + } 38 + 39 + export interface HandlerError { 40 + status: number; 41 + message?: string; 42 + error?: "RecordNotFound"; 43 + } 44 + 45 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 46 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 47 + auth: HA; 48 + params: QueryParams; 49 + input: HandlerInput; 50 + req: express.Request; 51 + res: express.Response; 52 + }; 53 + export type Handler<HA extends HandlerAuth = never> = ( 54 + ctx: HandlerReqCtx<HA>, 55 + ) => Promise<HandlerOutput> | HandlerOutput;
+77
src/lexicons/types/com/atproto/repo/listRecords.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import express from "express"; 5 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 6 + import { lexicons } from "../../../../lexicons"; 7 + import { isObj, hasProp } from "../../../../util"; 8 + import { CID } from "multiformats/cid"; 9 + import { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; 10 + 11 + export interface QueryParams { 12 + /** The handle or DID of the repo. */ 13 + repo: string; 14 + /** The NSID of the record type. */ 15 + collection: string; 16 + /** The number of records to return. */ 17 + limit: number; 18 + cursor?: string; 19 + /** DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) */ 20 + rkeyStart?: string; 21 + /** DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) */ 22 + rkeyEnd?: string; 23 + /** Flag to reverse the order of the returned records. */ 24 + reverse?: boolean; 25 + } 26 + 27 + export type InputSchema = undefined; 28 + 29 + export interface OutputSchema { 30 + cursor?: string; 31 + records: Record[]; 32 + [k: string]: unknown; 33 + } 34 + 35 + export type HandlerInput = undefined; 36 + 37 + export interface HandlerSuccess { 38 + encoding: "application/json"; 39 + body: OutputSchema; 40 + headers?: { [key: string]: string }; 41 + } 42 + 43 + export interface HandlerError { 44 + status: number; 45 + message?: string; 46 + } 47 + 48 + export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; 49 + export type HandlerReqCtx<HA extends HandlerAuth = never> = { 50 + auth: HA; 51 + params: QueryParams; 52 + input: HandlerInput; 53 + req: express.Request; 54 + res: express.Response; 55 + }; 56 + export type Handler<HA extends HandlerAuth = never> = ( 57 + ctx: HandlerReqCtx<HA>, 58 + ) => Promise<HandlerOutput> | HandlerOutput; 59 + 60 + export interface Record { 61 + uri: string; 62 + cid: string; 63 + value: {}; 64 + [k: string]: unknown; 65 + } 66 + 67 + export function isRecord(v: unknown): v is Record { 68 + return ( 69 + isObj(v) && 70 + hasProp(v, "$type") && 71 + v.$type === "com.atproto.repo.listRecords#record" 72 + ); 73 + } 74 + 75 + export function validateRecord(v: unknown): ValidationResult { 76 + return lexicons.validate("com.atproto.repo.listRecords#record", v); 77 + }
+14 -14
src/lexicons/types/com/atproto/repo/strongRef.ts
··· 1 1 /** 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 - import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { lexicons } from '../../../../lexicons' 6 - import { isObj, hasProp } from '../../../../util' 7 - import { CID } from 'multiformats/cid' 4 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../../lexicons"; 6 + import { isObj, hasProp } from "../../../../util"; 7 + import { CID } from "multiformats/cid"; 8 8 9 9 export interface Main { 10 - uri: string 11 - cid: string 12 - [k: string]: unknown 10 + uri: string; 11 + cid: string; 12 + [k: string]: unknown; 13 13 } 14 14 15 15 export function isMain(v: unknown): v is Main { 16 - return ( 17 - isObj(v) && 18 - hasProp(v, '$type') && 19 - (v.$type === 'com.atproto.repo.strongRef#main' || 20 - v.$type === 'com.atproto.repo.strongRef') 21 - ) 16 + return ( 17 + isObj(v) && 18 + hasProp(v, "$type") && 19 + (v.$type === "com.atproto.repo.strongRef#main" || 20 + v.$type === "com.atproto.repo.strongRef") 21 + ); 22 22 } 23 23 24 24 export function validateMain(v: unknown): ValidationResult { 25 - return lexicons.validate('com.atproto.repo.strongRef#main', v) 25 + return lexicons.validate("com.atproto.repo.strongRef#main", v); 26 26 }
+29
src/lexicons/types/li/plonk/comment.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../lexicons"; 6 + import { isObj, hasProp } from "../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + import * as ComAtprotoRepoStrongRef from "../../com/atproto/repo/strongRef"; 9 + 10 + export interface Record { 11 + /** comment body */ 12 + content: string; 13 + /** comment creation timestamp */ 14 + createdAt: string; 15 + post: ComAtprotoRepoStrongRef.Main; 16 + [k: string]: unknown; 17 + } 18 + 19 + export function isRecord(v: unknown): v is Record { 20 + return ( 21 + isObj(v) && 22 + hasProp(v, "$type") && 23 + (v.$type === "li.plonk.comment#main" || v.$type === "li.plonk.comment") 24 + ); 25 + } 26 + 27 + export function validateRecord(v: unknown): ValidationResult { 28 + return lexicons.validate("li.plonk.comment#main", v); 29 + }
+28
src/lexicons/types/li/plonk/paste.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from "@atproto/lexicon"; 5 + import { lexicons } from "../../../lexicons"; 6 + import { isObj, hasProp } from "../../../util"; 7 + import { CID } from "multiformats/cid"; 8 + 9 + export interface Record { 10 + code: string; 11 + shortUrl: string; 12 + lang: string; 13 + title: string; 14 + createdAt: string; 15 + [k: string]: unknown; 16 + } 17 + 18 + export function isRecord(v: unknown): v is Record { 19 + return ( 20 + isObj(v) && 21 + hasProp(v, "$type") && 22 + (v.$type === "li.plonk.paste#main" || v.$type === "li.plonk.paste") 23 + ); 24 + } 25 + 26 + export function validateRecord(v: unknown): ValidationResult { 27 + return lexicons.validate("li.plonk.paste#main", v); 28 + }
-27
src/lexicons/types/ovh/plonk/paste.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { lexicons } from '../../../lexicons' 6 - import { isObj, hasProp } from '../../../util' 7 - import { CID } from 'multiformats/cid' 8 - 9 - export interface Record { 10 - code: string 11 - lang: string 12 - title: string 13 - createdAt: string 14 - [k: string]: unknown 15 - } 16 - 17 - export function isRecord(v: unknown): v is Record { 18 - return ( 19 - isObj(v) && 20 - hasProp(v, '$type') && 21 - (v.$type === 'ovh.plonk.paste#main' || v.$type === 'ovh.plonk.paste') 22 - ) 23 - } 24 - 25 - export function validateRecord(v: unknown): ValidationResult { 26 - return lexicons.validate('ovh.plonk.paste#main', v) 27 - }
+4 -4
src/lexicons/util.ts
··· 2 2 * GENERATED CODE - DO NOT MODIFY 3 3 */ 4 4 export function isObj(v: unknown): v is Record<string, unknown> { 5 - return typeof v === 'object' && v !== null 5 + return typeof v === "object" && v !== null; 6 6 } 7 7 8 8 export function hasProp<K extends PropertyKey>( 9 - data: object, 10 - prop: K, 9 + data: object, 10 + prop: K, 11 11 ): data is Record<K, unknown> { 12 - return prop in data 12 + return prop in data; 13 13 }
+48
src/lib/highlight.ts
··· 1 + import { createHighlighter, HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki' 2 + import type { Element } from 'hast' 3 + 4 + let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null 5 + 6 + export async function getHighlighterInstance() { 7 + if (!highlighter) { 8 + highlighter = await createHighlighter({ 9 + themes: ['catppuccin-latte', 'catppuccin-mocha'], 10 + langs: [ 11 + 'javascript', 'typescript', 'python', 'java', 'rust', 12 + 'go', 'c', 'cpp', 'csharp', 'php', 'ruby', 'cobol', 13 + 'plaintext', 'txt', 'bash', 'nix' 14 + ] 15 + }) 16 + } 17 + return highlighter 18 + } 19 + 20 + export async function highlightCode(code: string, lang: string) { 21 + const highlighter = await getHighlighterInstance() 22 + 23 + try { 24 + return highlighter.codeToHtml(code, { 25 + lang: lang, 26 + themes: { 27 + light: 'catppuccin-latte', 28 + dark: 'catppuccin-mocha' 29 + }, 30 + transformers: [ 31 + { 32 + line(node: Element, line: number) { 33 + node.properties['code-line'] = line 34 + } 35 + } 36 + ] 37 + }) 38 + } catch (error) { 39 + console.warn(`Language "${lang}" not supported by Shiki, falling back to plaintext`) 40 + return highlighter.codeToHtml(code, { 41 + lang: 'txt', 42 + themes: { 43 + light: 'catppuccin-latte', 44 + dark: 'catppuccin-mocha' 45 + } 46 + }) 47 + } 48 + }
+6 -6
src/lib.ts
··· 4 4 dotenv.config(); 5 5 6 6 export const env = cleanEnv(process.env, { 7 - NODE_ENV: str({ 7 + PLONK_NODE_ENV: str({ 8 8 devDefault: testOnly("test"), 9 9 choices: ["development", "production", "test"], 10 10 }), 11 - HOST: host({ devDefault: testOnly("localhost") }), 12 - PORT: port({ devDefault: testOnly(3000) }), 13 - PUBLIC_URL: str({}), 14 - DB_PATH: str({ devDefault: ":memory:" }), 15 - COOKIE_SECRET: str({ devDefault: "00000000000000000000000000000001" }), 11 + PLONK_HOST: host({ devDefault: testOnly("localhost") }), 12 + PLONK_PORT: port({ devDefault: testOnly(3000) }), 13 + PLONK_PUBLIC_URL: str({}), 14 + PLONK_DB_PATH: str({ devDefault: ":memory:" }), 15 + PLONK_COOKIE_SECRET: str({ devDefault: "00000000000000000000000000000000" }), 16 16 });
+9
src/mixins/footer.pug
··· 1 + mixin footer() 2 + hr 3 + div.footer 4 + div.left-side 5 + div.right-side 6 + p 7 + | made by 8 + a(href="https://bsky.app/profile/oppi.li") @oppi.li 9 +
+1 -1
src/mixins/head.pug
··· 2 2 head 3 3 meta(name="viewport" content="width=device-width, initial-scale=1.0") 4 4 meta(charset='UTF-8') 5 - title #{title} 5 + title #{title} ยท plonk.li 6 6 link(rel="stylesheet", href="/styles.css") 7 7 link(rel="preconnect" href="https://rsms.me/") 8 8 link(rel="stylesheet" href="https://rsms.me/inter/inter.css")
+15
src/mixins/header.pug
··· 1 + mixin header(ownDid, didHandleMap) 2 + div.header 3 + div.left-side 4 + div.right-side 5 + p 6 + a(href="/") home 7 + | &nbsp;ยท&nbsp; 8 + if ownDid 9 + a(href=`/u/${encodeURIComponent(ownDid)}`) my plonks 10 + | &nbsp;ยท&nbsp; 11 + a(href="/logout") logout 12 + else 13 + a(href="/login") login 14 + |&nbsp;to get plonkin' 15 +
+6 -5
src/mixins/post.pug
··· 1 - mixin post(paste, didHandleMap) 1 + mixin post(paste, handle, did) 2 2 div.post 3 3 p 4 - a(href=`/p/${paste.shortUrl}`) 4 + a(href=`/p/${paste.shortUrl}`).post-link 5 5 | #{paste.title} 6 6 p.post-info 7 7 | by 8 - a(href=`/u/${encodeURIComponent(paste.authorDid)}`) 9 - | @#{didHandleMap[paste.authorDid]} 8 + a(href=`/u/${did}`) 9 + | @#{handle} 10 10 | &nbsp;ยท 11 11 | #{timeDifference(now, Date.parse(paste.createdAt))} ago 12 12 | ยท 13 13 | #{paste.lang} 14 14 | ยท 15 15 | #{paste.code.split('\n').length} loc 16 - 16 + | ยท&nbsp; 17 + a(href=`/p/${paste.shortUrl}/#comments`) #{paste.commentCount} #{pluralize(paste.commentCount, 'comment')}
+4
src/mixins/utils.pug
··· 2 2 function randInt(min, max) { 3 3 return Math.floor(Math.random() * (max - min + 1)) + min; 4 4 } 5 + - 6 + function pluralize(count, noun) { 7 + return count==1?noun:`${noun}s`; 8 + } 5 9 - 6 10 function timeDifference(current, previous) { 7 11 if (!current || !previous) {
+142 -6
src/public/styles.css
··· 1 - @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap'); 1 + @font-face { 2 + font-family: 'NerdIosevka'; 3 + src: url('../assets/NerdIosevka-Regular.woff2') format('woff2'); 4 + font-weight: normal; 5 + font-style: monospace; 6 + } 7 + 8 + @media (prefers-color-scheme: dark) { 9 + .shiki, 10 + .shiki span { 11 + color: var(--shiki-dark) !important; 12 + background-color: var(--shiki-dark-bg) !important; 13 + font-style: var(--shiki-dark-font-style) !important; 14 + font-weight: var(--shiki-dark-font-weight) !important; 15 + text-decoration: var(--shiki-dark-text-decoration) !important; 16 + } 17 + } 2 18 3 19 :root { 4 20 /* Light mode colors */ ··· 28 44 } 29 45 30 46 * { 31 - font-family: 'IBM Plex Mono', monospace; 47 + font-family: 'NerdIosevka', monospace; 48 + font-size: 0.9rem; 32 49 } 33 50 34 51 body { ··· 52 69 } 53 70 54 71 pre { 55 - background-color: var(--bg-color-muted); 56 72 padding: 1rem; 57 73 overflow-x: auto; 58 74 } 59 75 76 + .comment-body { 77 + background-color: var(--bg-color); 78 + padding: 0; 79 + margin-top: 0.1rem; 80 + } 81 + 82 + .comment-info { 83 + margin-bottom: 0; 84 + } 85 + 60 86 input, textarea, select, button { 61 87 border: none; 62 88 padding: 1rem; ··· 93 119 hr { 94 120 border: none; 95 121 border-top: 1px solid var(--bg-color-muted); 96 - padding: 1rem; 97 122 } 98 123 99 124 .post-form { ··· 127 152 align-self: flex-end; 128 153 } 129 154 130 - .timeline { 155 + .post-link { 156 + color: var(--text-color); 157 + text-decoration: none; 158 + } 159 + .post-link:hover { 160 + text-decoration: underline; 161 + } 162 + .post-link:visited { 163 + color: var(--text-color-muted); 164 + } 165 + 166 + .post-info { 167 + margin-top: 0; 168 + } 169 + 170 + .post-info, .comment-info { 171 + color: var(--text-color-muted); 172 + } 173 + .post-info a, .comment-info a { 174 + color: var(--text-color-muted); 175 + text-decoration: none; 176 + } 177 + .post-info a:visited, .comment-info a:visited { 178 + color: var(--text-color-muted); 179 + } 180 + .post-info a:hover, .comment-info a:hover { 181 + text-decoration: underline; 182 + } 183 + 184 + .timeline, .comments { 131 185 display: flex; 132 186 flex-direction: column; 133 187 gap: 1rem; 188 + padding-bottom: 1rem; 134 189 } 135 190 136 191 .login-input-title { ··· 141 196 flex: 1 142 197 } 143 198 144 - .header { 199 + .header, .footer { 145 200 display: flex; 146 201 flex-direction: row; 147 202 justify-content: space-between; 148 203 align-items: center; 149 204 } 205 + 206 + select { 207 + -webkit-appearance: none; 208 + -moz-appearance: none; 209 + text-indent: 1px; 210 + text-overflow: ''; 211 + } 212 + 213 + .code-line { 214 + display: flex; 215 + } 216 + 217 + .code-line-num { 218 + white-space: pre; 219 + -webkit-user-select: none; 220 + user-select: none; 221 + margin-right: 0.4em; 222 + padding: 0 0.4em 0 0.4em; 223 + color: var(--text-color-muted); 224 + text-align: right; 225 + } 226 + 227 + .code-line-content { 228 + color: var(--text-color); 229 + } 230 + 231 + .header, .footer { 232 + color: var(--text-color); 233 + } 234 + 235 + .header a, .header a:visited, 236 + .footer a, .footer a:visited { 237 + color: var(--link-color); 238 + text-decoration: none; 239 + } 240 + 241 + .header a:hover, 242 + .footer a:hover { 243 + text-decoration: underline; 244 + } 245 + 246 + .highlighted-code { 247 + margin: 1rem 0; 248 + counter-reset: step; 249 + counter-increment: step calc(var(--start, 1) - 1); 250 + } 251 + 252 + .highlighted-code pre { 253 + padding: 1rem; 254 + overflow-x: auto; 255 + border-radius: 4px; 256 + } 257 + 258 + code .line::before { 259 + content: counter(step); 260 + counter-increment: step; 261 + width: 1rem; 262 + margin-right: 1.5rem; 263 + display: inline-block; 264 + text-align: right; 265 + color: rgba(115,138,148,.4) 266 + } 267 + 268 + #copy-btn { 269 + background: none; 270 + border: none; 271 + padding: 0; 272 + color: var(--text-color-muted); 273 + cursor: pointer; 274 + text-decoration: none; 275 + font-family: inherit; 276 + font-size: inherit; 277 + } 278 + 279 + #copy-btn:hover { 280 + text-decoration: underline; 281 + } 282 + 283 + #copy-btn:focus { 284 + outline: 1px solid var(--accent); 285 + }
+246 -66
src/routes.ts
··· 6 6 import { isValidHandle, AtUri } from "@atproto/syntax"; 7 7 import { IncomingMessage, ServerResponse } from "node:http"; 8 8 import { Agent } from "@atproto/api"; 9 - import { getPds, DidResolver } from "@atproto/identity"; 10 9 import { TID } from "@atproto/common"; 11 10 import { newShortUrl } from "#/db"; 12 11 13 - import * as Paste from "#/lexicons/types/ovh/plonk/paste"; 12 + import * as Paste from "#/lexicons/types/li/plonk/paste"; 13 + import * as Comment from "#/lexicons/types/li/plonk/comment"; 14 + import { highlightCode } from "#/lib/highlight"; 14 15 15 16 type Session = { 16 17 did: string; 17 18 }; 18 19 20 + async function getSession( 21 + req: IncomingMessage, 22 + res: ServerResponse<IncomingMessage>, 23 + ) { 24 + return await getIronSession<Session>(req, res, { 25 + cookieName: "plonk-id", 26 + password: env.PLONK_COOKIE_SECRET, 27 + cookieOptions: { 28 + secure: env.PLONK_NODE_ENV === "production", 29 + }, 30 + }); 31 + } 32 + 19 33 async function getSessionAgent( 20 34 req: IncomingMessage, 21 35 res: ServerResponse<IncomingMessage>, 22 36 ctx: Ctx, 23 37 ) { 24 - const session = await getIronSession<Session>(req, res, { 25 - cookieName: "plonk-id", 26 - password: env.COOKIE_SECRET, 27 - }); 38 + const session = await getSession(req, res); 28 39 if (!session.did) return null; 29 40 try { 30 41 const oauthSession = await ctx.oauthClient.restore(session.did); ··· 39 50 export const createRouter = (ctx: Ctx) => { 40 51 const router = express.Router(); 41 52 42 - // Static assets 43 - router.use( 44 - "/public", 45 - express.static(path.join(__dirname, "pages", "public")), 46 - ); 47 - 53 + router.use("/assets", express.static(path.join(__dirname, "assets"))); 48 54 // OAuth metadata 49 55 router.get("/client-metadata.json", async (_req, res) => { 50 56 return res.json(ctx.oauthClient.clientMetadata); ··· 54 60 const params = new URLSearchParams(req.originalUrl.split("?")[1]); 55 61 try { 56 62 const { session } = await ctx.oauthClient.callback(params); 57 - const clientSession = await getIronSession<Session>(req, res, { 58 - cookieName: "plonk-id", 59 - password: env.COOKIE_SECRET, 60 - }); 61 - ctx.logger.info(clientSession.did, "client session did"); 63 + const clientSession = await getSession(req, res); 62 64 //assert(!clientSession.did, "session already exists"); 63 65 clientSession.did = session.did; 64 66 await clientSession.save(); ··· 95 97 }); 96 98 97 99 router.get("/logout", async (req, res) => { 98 - const session = await getIronSession<Session>(req, res, { 99 - cookieName: "plonk-id", 100 - password: env.COOKIE_SECRET, 101 - }); 100 + const session = await getSession(req, res); 102 101 session.destroy(); 103 102 return res.redirect("/"); 104 103 }); ··· 107 106 const agent = await getSessionAgent(req, res, ctx); 108 107 const pastes = await ctx.db 109 108 .selectFrom("paste") 110 - .selectAll() 111 - .orderBy("indexedAt", "desc") 109 + .leftJoin("comment", "comment.pasteUri", "paste.uri") 110 + .select([ 111 + "paste.uri", 112 + "paste.shortUrl", 113 + "paste.authorDid", 114 + "paste.code", 115 + "paste.lang", 116 + "paste.title", 117 + "paste.createdAt", 118 + "paste.indexedAt as pasteIndexedAt", 119 + ctx.db.fn.count("comment.uri").as("commentCount") 120 + ]) 121 + .groupBy("paste.uri") 122 + .orderBy("pasteIndexedAt", "desc") 112 123 .limit(25) 113 124 .execute(); 114 125 115 126 // Map user DIDs to their domain-name handles 116 127 const didHandleMap = await ctx.resolver.resolveDidsToHandles( 117 - pastes.map((s) => s.authorDid), 128 + pastes.map((s) => s.authorDid).concat(agent ? [agent.assertDid] : []), 118 129 ); 119 130 120 131 if (!agent) { ··· 132 143 const { authorDid } = req.params; 133 144 const pastes = await ctx.db 134 145 .selectFrom("paste") 135 - .selectAll() 136 - .where("authorDid", "=", authorDid) 137 - .orderBy("indexedAt", "desc") 146 + .leftJoin("comment", "comment.pasteUri", "paste.uri") 147 + .select([ 148 + "paste.uri", 149 + "paste.shortUrl", 150 + "paste.authorDid as pasteAuthorDid", 151 + "paste.code", 152 + "paste.lang", 153 + "paste.title", 154 + "paste.createdAt as pasteCreatedAt", 155 + "paste.indexedAt as pasteIndexedAt", 156 + ctx.db.fn.count("comment.uri").as("commentCount") 157 + ]) 158 + .groupBy("paste.uri") 159 + .where("pasteAuthorDid", "=", authorDid) 160 + .orderBy("pasteCreatedAt", "desc") 138 161 .execute(); 139 - let didHandleMap = {}; 162 + let didHandleMap: Record<string, string> = {}; 140 163 didHandleMap[authorDid] = await ctx.resolver.resolveDidToHandle(authorDid); 141 - return res.render("user", { pastes, authorDid, didHandleMap }); 164 + const ownAgent = await getSessionAgent(req, res, ctx); 165 + if (!ownAgent) { 166 + return res.render("user", { pastes, authorDid, didHandleMap }); 167 + } else { 168 + const ownDid = ownAgent.assertDid; 169 + didHandleMap[ownDid] = await ctx.resolver.resolveDidToHandle(ownDid); 170 + return res.render("user", { pastes, authorDid, ownDid, didHandleMap }); 171 + } 142 172 }); 143 173 144 174 router.get("/p/:shortUrl", async (req, res) => { 145 175 const { shortUrl } = req.params; 146 176 const ret = await ctx.db 147 177 .selectFrom("paste") 178 + .leftJoin("comment", "comment.pasteUri", "paste.uri") 179 + .select([ 180 + "paste.uri as pasteUri", 181 + "comment.pasteCid as pasteCid", 182 + "paste.authorDid as pasteAuthorDid", 183 + "paste.code as pasteCode", 184 + "paste.lang as pasteLang", 185 + "paste.title as pasteTitle", 186 + "paste.createdAt as pasteCreatedAt", 187 + "comment.uri as commentUri", 188 + "comment.authorDid as commentAuthorDid", 189 + "comment.body as commentBody", 190 + "comment.createdAt as commentCreatedAt", 191 + ]) 148 192 .where("shortUrl", "=", shortUrl) 149 - .select(["authorDid", "uri"]) 150 - .executeTakeFirst(); 151 - if (!ret) { 152 - return res.status(404); 153 - } 154 - const { authorDid: did, uri } = ret; 155 - const handle = await ctx.resolver.resolveDidToHandle(did); 156 - const resolver = new DidResolver({}); 157 - const didDocument = await resolver.resolve(did); 158 - if (!didDocument) { 159 - return res.status(404); 160 - } 161 - const pds = getPds(didDocument); 162 - if (!pds) { 193 + .execute(); 194 + if (ret.length === 0) { 163 195 return res.status(404); 164 196 } 165 - const aturi = new AtUri(uri); 166 - const url = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`); 167 - url.searchParams.set("repo", aturi.hostname); 168 - url.searchParams.set("collection", aturi.collection); 169 - url.searchParams.set("rkey", aturi.rkey); 197 + const { 198 + pasteAuthorDid, 199 + pasteUri, 200 + pasteCode, 201 + pasteLang, 202 + pasteTitle, 203 + pasteCreatedAt, 204 + } = ret[0]; 205 + let didHandleMap = await ctx.resolver.resolveDidsToHandles( 206 + [ret[0].pasteAuthorDid].concat( 207 + ret.flatMap((row) => 208 + row.commentAuthorDid ? [row.commentAuthorDid] : [], 209 + ), 210 + ), 211 + ); 170 212 171 - const response = await fetch(url.toString()); 213 + const highlightedCode = await highlightCode(pasteCode, pasteLang); 172 214 173 - if (!response.ok) { 174 - return res.status(404); 175 - } 215 + const paste = { 216 + uri: pasteUri, 217 + code: pasteCode, 218 + highlightedCode, 219 + title: pasteTitle, 220 + lang: pasteLang, 221 + shortUrl, 222 + createdAt: pasteCreatedAt, 223 + authorDid: pasteAuthorDid, 224 + }; 176 225 177 - const pasteRecord = await response.json(); 178 - const paste = 179 - Paste.isRecord(pasteRecord.value) && 180 - Paste.validateRecord(pasteRecord.value).success 181 - ? pasteRecord.value 182 - : {}; 226 + const comments = ret 227 + .filter((row) => row.commentUri) 228 + .map((row) => { 229 + return { 230 + uri: row.commentUri, 231 + authorDid: row.commentAuthorDid, 232 + body: row.commentBody, 233 + createdAt: row.commentCreatedAt, 234 + }; 235 + }); 183 236 184 - return res.render("paste", { paste, handle, shortUrl }); 237 + const ownAgent = await getSessionAgent(req, res, ctx); 238 + if (!ownAgent) { 239 + return res.render("paste", { 240 + paste, 241 + didHandleMap, 242 + comments, 243 + }); 244 + } else { 245 + const ownDid = ownAgent.assertDid; 246 + didHandleMap[ownDid] = await ctx.resolver.resolveDidToHandle(ownDid); 247 + return res.render("paste", { 248 + paste, 249 + ownDid, 250 + didHandleMap, 251 + comments, 252 + }); 253 + } 185 254 }); 186 255 256 + router.get("/p/:shortUrl/raw", async (req, res) => { 257 + res.redirect(`/r/${req.params.shortUrl}`); 258 + }); 187 259 router.get("/r/:shortUrl", async (req, res) => { 188 260 const { shortUrl } = req.params; 189 261 const ret = await ctx.db ··· 199 271 return res.send(ret.code); 200 272 }); 201 273 274 + router.get("/reset", async (req, res) => { 275 + const agent = await getSessionAgent(req, res, ctx); 276 + if (!agent) { 277 + return res.redirect("/"); 278 + } 279 + const response = await agent.com.atproto.repo.listRecords({ 280 + repo: agent.assertDid, 281 + collection: "li.plonk.paste", 282 + limit: 10, 283 + }); 284 + const vals = response.data.records; 285 + for (const v of vals) { 286 + const aturl = new AtUri(v.uri); 287 + await agent.com.atproto.repo.deleteRecord({ 288 + repo: agent.assertDid, 289 + collection: aturl.collection, 290 + rkey: aturl.rkey, 291 + }); 292 + } 293 + return res.redirect("/"); 294 + }); 295 + 202 296 router.post("/paste", async (req, res) => { 203 297 const agent = await getSessionAgent(req, res, ctx); 204 298 if (!agent) { ··· 209 303 } 210 304 211 305 const rkey = TID.nextStr(); 306 + const shortUrl = await newShortUrl(ctx.db); 212 307 const record = { 213 - $type: "ovh.plonk.paste", 308 + $type: "li.plonk.paste", 214 309 code: req.body?.code, 215 310 lang: req.body?.lang, 311 + shortUrl, 216 312 title: req.body?.title, 217 313 createdAt: new Date().toISOString(), 218 314 }; ··· 228 324 try { 229 325 const res = await agent.com.atproto.repo.putRecord({ 230 326 repo: agent.assertDid, 231 - collection: "ovh.plonk.paste", 327 + collection: "li.plonk.paste", 232 328 rkey, 233 329 record, 234 330 validate: false, ··· 257 353 indexedAt: new Date().toISOString(), 258 354 }) 259 355 .execute(); 260 - ctx.logger.info(res, "wrote back to db"); 261 356 return res.redirect(`/p/${shortUrl}`); 262 357 } catch (err) { 263 358 ctx.logger.warn( ··· 269 364 return res.redirect("/"); 270 365 }); 271 366 367 + router.post("/:paste/comment", async (req, res) => { 368 + const agent = await getSessionAgent(req, res, ctx); 369 + 370 + if (!agent) { 371 + return res 372 + .status(401) 373 + .type("html") 374 + .send("<h1>Error: Session required</h1>"); 375 + } 376 + 377 + const pasteUri = req.params.paste; 378 + const aturi = new AtUri(pasteUri); 379 + const pasteResponse = await agent.com.atproto.repo.getRecord({ 380 + repo: aturi.hostname, 381 + collection: aturi.collection, 382 + rkey: aturi.rkey, 383 + }); 384 + const pasteCid = pasteResponse.data.cid; 385 + if (!pasteCid) { 386 + return res.status(401).type("html").send("invalid paste"); 387 + } 388 + 389 + const rkey = TID.nextStr(); 390 + const record = { 391 + $type: "li.plonk.comment", 392 + content: req.body?.comment, 393 + post: { 394 + uri: pasteUri, 395 + cid: pasteCid, 396 + }, 397 + createdAt: new Date().toISOString(), 398 + }; 399 + 400 + if (!Comment.validateRecord(record).success) { 401 + return res 402 + .status(400) 403 + .type("html") 404 + .send("<h1>Error: Invalid status</h1>"); 405 + } 406 + 407 + let uri; 408 + try { 409 + const res = await agent.com.atproto.repo.putRecord({ 410 + repo: agent.assertDid, 411 + collection: "li.plonk.comment", 412 + rkey, 413 + record, 414 + validate: false, 415 + }); 416 + uri = res.data.uri; 417 + } catch (err) { 418 + ctx.logger.warn({ err }, "failed to put record"); 419 + return res 420 + .status(500) 421 + .type("html") 422 + .send("<h3>Error: Failed to write record</h1>"); 423 + } 424 + 425 + try { 426 + await ctx.db 427 + .insertInto("comment") 428 + .values({ 429 + uri, 430 + body: record.content, 431 + authorDid: agent.assertDid, 432 + pasteUri: record.post.uri, 433 + pasteCid: record.post.cid, 434 + createdAt: record.createdAt, 435 + indexedAt: new Date().toISOString(), 436 + }) 437 + .execute(); 438 + const originalPaste = await ctx.db 439 + .selectFrom("paste") 440 + .selectAll() 441 + .where("uri", "=", pasteUri) 442 + .executeTakeFirst(); 443 + return res.redirect( 444 + `/p/${originalPaste.shortUrl}#${encodeURIComponent(uri)}`, 445 + ); 446 + } catch (err) { 447 + ctx.logger.warn( 448 + { err }, 449 + "failed to update computed view; ignoring as it should be caught by the firehose", 450 + ); 451 + } 452 + 453 + return res.redirect("/"); 454 + }); 455 + 272 456 return router; 273 457 }; 274 - 275 - // https://pds.icyphox.sh/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3A3ft67n4xnawzq4qi7mcksxj5 276 - // at://did:plc:3ft67n4xnawzq4qi7mcksxj5/ovh.plonk.paste/3lcs3lnslbk2d 277 - // https://pds.icyphox.sh/xrpc/com.atproto.repo.getRecord?repo=did%3Aplc%3A3ft67n4xnawzq4qi7mcksxj5&collection=ovh.plonk.paste&rkey=3lcqt7newvc2c
+10 -15
src/views/index.pug
··· 1 1 include ../mixins/mkPost 2 2 include ../mixins/head 3 + include ../mixins/header 4 + include ../mixins/footer 3 5 include ../mixins/utils 4 6 include ../mixins/post 5 7 ··· 7 9 - 8 10 var langs = ["plaintext"] 9 11 .concat([ 12 + "bash", 10 13 "javascript", 11 14 "typescript", 12 15 "java", ··· 18 21 "c", 19 22 "c#", 20 23 "c++", 24 + "cobol", 25 + "nix", 21 26 ].toSorted()) 22 27 doctype html 23 28 html 24 29 +head("timeline") 25 30 body 26 31 main#content 27 - div.header 28 - div.left-side 29 - if ownDid 30 - | logged in as @#{didHandleMap[ownDid]} 31 - div.right-side 32 - if ownDid 33 - p 34 - a(href=`/u/${encodeURIComponent(ownDid)}`) my plonks 35 - | &nbsp;ยท&nbsp; 36 - a(href="/logout") logout 37 - else 38 - p 39 - a(href="/login") login 40 - |&nbsp;to get plonkin' 32 + +header(ownDid, didHandleMap) 41 33 42 34 if ownDid 43 35 +mkPost() 44 36 45 37 div.timeline 46 38 each paste in pastes 47 - +post(paste, didHandleMap) 39 + - var handle = didHandleMap[paste.authorDid] 40 + +post(paste, handle, paste.authorDid) 41 + 42 + +footer()
+5 -8
src/views/login.pug
··· 1 + include ../mixins/head 2 + include ../mixins/footer 3 + 1 4 doctype html 2 5 html 3 - head 4 - meta(name="viewport" content="width=device-width, initial-scale=1.0") 5 - meta(charset='UTF-8') 6 - title login 7 - link(rel="stylesheet", href="/styles.css") 8 - link(rel="preconnect" href="https://rsms.me/") 9 - link(rel="stylesheet" href="https://rsms.me/inter/inter.css") 10 - script(src="https://cdn.dashjs.org/latest/dash.all.min.js") 6 + +head("login") 11 7 body 12 8 main#content 13 9 h1 login ··· 15 11 div.login-row 16 12 input(type="text" name="handle" placeholder="enter handle" required).login-input-title 17 13 button(type="submit").login-submit-button login 14 + +footer()
+62 -6
src/views/paste.pug
··· 1 1 - var now = new Date() 2 2 include ../mixins/head 3 + include ../mixins/header 4 + include ../mixins/footer 3 5 include ../mixins/utils 4 6 doctype html 5 7 html 6 - +head(paste.title) 8 + +head(`${paste.title} ยท ${didHandleMap[paste.authorDid]}`) 7 9 body 8 10 main#content 11 + +header(ownDid, didHandleMap) 9 12 h1 #{paste.title} 10 - p 11 - | by @#{handle} ยท 13 + p.post-info 14 + | @#{didHandleMap[paste.authorDid]} ยท 12 15 | #{timeDifference(now, Date.parse(paste.createdAt))} ago ยท 13 16 | #{paste.lang} ยท 14 17 | #{paste.code.split('\n').length} loc ยท 15 - a(href=`/r/${shortUrl}`) raw 16 - pre 17 - | #{paste.code} 18 + a(href=`/r/${paste.shortUrl}`) raw 19 + | &nbsp;ยท&nbsp; 20 + button#copy-btn(type="button" onclick="copyToClipboard()" data-code=paste.code) copy 21 + | &nbsp;ยท 22 + | #{comments.length} #{pluralize(comments.length, 'comment')} 23 + div.highlighted-code !{paste.highlightedCode} 24 + hr 25 + 26 + if comments.length != 0 27 + h1(id="comments") comments 28 + div.comments 29 + each comment in comments 30 + div.comment(id=`${encodeURIComponent(comment.uri)}`) 31 + p.comment-info 32 + a(href=`/u/${comment.authorDid}`) 33 + | @#{didHandleMap[comment.authorDid]} 34 + | &nbsp;ยท 35 + | #{timeDifference(now, Date.parse(paste.createdAt))} ago 36 + pre.comment-body #{comment.body} 37 + 38 + if ownDid 39 + form(action=`/${encodeURIComponent(paste.uri)}/comment` method="post").post-form 40 + div.post-row 41 + textarea#code(name="comment" rows="5" placeholder="add a comment" required).post-input-code 42 + 43 + div.post-submit-row 44 + button(type="submit").post-input-submit zonk! 45 + else 46 + p 47 + a(href="/login") login 48 + |&nbsp;to post a comment 49 + 50 + +footer() 51 + script. 52 + async function copyToClipboard() { 53 + const copyBtn = document.getElementById('copy-btn'); 54 + const originalText = copyBtn.textContent; 55 + 56 + try { 57 + const code = copyBtn.dataset.code; 58 + await navigator.clipboard.writeText(code); 59 + copyBtn.textContent = 'copied!'; 60 + copyBtn.style.color = 'var(--accent)'; 61 + 62 + setTimeout(() => { 63 + copyBtn.textContent = originalText; 64 + copyBtn.style.color = ''; 65 + }, 1500); 66 + } catch (err) { 67 + console.error('Failed to copy: ', err); 68 + copyBtn.textContent = 'copy failed'; 69 + setTimeout(() => { 70 + copyBtn.textContent = originalText; 71 + }, 1500); 72 + } 73 + }
+6 -1
src/views/user.pug
··· 1 1 - var now = new Date() 2 2 - var handle = didHandleMap[authorDid] 3 3 include ../mixins/head 4 + include ../mixins/header 5 + include ../mixins/footer 4 6 include ../mixins/utils 5 7 include ../mixins/post 6 8 doctype html ··· 8 10 +head(handle) 9 11 body 10 12 main#content 13 + +header(ownDid, didHandleMap) 11 14 h1 plonks by @#{handle} 12 15 div.timeline 13 16 each paste in pastes 14 - +post(paste, didHandleMap) 17 + +post(paste, handle, authorDid) 18 + 19 + +footer()