A simple AtProto app to read pet.mewsse.link records on my PDS.

Initial commit

Mewsse 9f78dde7

+3
.env.example
···
+
DB_PATH=":memory:"
+
DID="did:plc:you_did_here"
+
LOG_LEVEL="INFO"
+3
.gitignore
···
+
node_modules
+
.env
+
*.db
+1
.nvmrc
···
+
24
+13
LICENCE
···
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+
Version 2, December 2004
+
+
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
+
+
Everyone is permitted to copy and distribute verbatim or modified
+
copies of this license document, and changing it is allowed as long
+
as the name is changed.
+
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+
0. You just DO WHAT THE FUCK YOU WANT TO.
+6
README.md
···
+
# pet.mewsse.link
+
+
A simple AtProto app to read `pet.mewsse.link` records on my PDS.
+
AppView will come later.
+
+
Built with [atcute](https://codeberg.org/mary-ext/atcute) and [skyware](https://skyware.js.org)
+6
lex.config.js
···
+
import { defineLexiconConfig } from '@atcute/lex-cli';
+
+
export default defineLexiconConfig({
+
files: ['lexicons/**/*.json'],
+
outdir: 'src/lexicons/',
+
});
+51
lexicons/pet/mewsse/link.json
···
+
{
+
"lexicon": 1,
+
"id": "pet.mewsse.link",
+
"defs": {
+
"main": {
+
"type": "record",
+
"description": "Record containing a link.",
+
"key": "tid",
+
"record": {
+
"type": "object",
+
"required": ["title", "link", "createdAt"],
+
"properties": {
+
"link": {
+
"type": "string",
+
"maxLength": 3000,
+
"description": "The link to point to."
+
},
+
"title": {
+
"type": "string",
+
"maxLength": 3000,
+
"description": "Title of the given link."
+
},
+
"description": {
+
"type": "string",
+
"description": "Short description for the content of the link."
+
},
+
"tag": {
+
"type": "string",
+
"description": "A tag for classify the link."
+
},
+
"image": {
+
"type": "blob",
+
"description": "An image to illustrate the link",
+
"accept": ["image/*"],
+
"maxSize": 1000000
+
},
+
"alt": {
+
"type": "string",
+
"maxLength": 3000,
+
"description": "A alt text to describe the image."
+
},
+
"createdAt": {
+
"type": "string",
+
"format": "datetime",
+
"description": "Client-declared timestamp when this post was originally created."
+
}
+
}
+
}
+
}
+
}
+
}
+780
package-lock.json
···
+
{
+
"name": "mewsse-links",
+
"version": "0.0.1",
+
"lockfileVersion": 3,
+
"requires": true,
+
"packages": {
+
"": {
+
"name": "mewsse-links",
+
"version": "0.0.1",
+
"license": "WTFPL",
+
"dependencies": {
+
"@atcute/atproto": "^3.1.7",
+
"@atcute/car": "^3.1.2",
+
"@atcute/cbor": "^2.2.6",
+
"@atcute/client": "^4.0.4",
+
"@atcute/identity-resolver": "^1.1.4",
+
"@atcute/lex-cli": "^2.2.2",
+
"@atcute/lexicons": "^1.2.2",
+
"@skyware/jetstream": "^0.2.5",
+
"better-sqlite3": "^12.4.1",
+
"dotenv": "^17.2.3",
+
"kysely": "^0.28.7"
+
},
+
"devDependencies": {
+
"@types/better-sqlite3": "^7.6.13",
+
"@types/node": "^24.7.1"
+
}
+
},
+
"node_modules/@atcute/atproto": {
+
"version": "3.1.7",
+
"resolved": "https://registry.npmjs.org/@atcute/atproto/-/atproto-3.1.7.tgz",
+
"integrity": "sha512-3Ym8qaVZg2vf8qw0KO1aue39z/5oik5J+UDoSes1vr8ddw40UVLA5sV4bXSKmLnhzQHiLLgoVZXe4zaKfozPoQ==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/lexicons": "^1.2.2"
+
}
+
},
+
"node_modules/@atcute/bluesky": {
+
"version": "3.2.6",
+
"resolved": "https://registry.npmjs.org/@atcute/bluesky/-/bluesky-3.2.6.tgz",
+
"integrity": "sha512-jUSSTW5Th1vy0bWBazVHuhGQ3Xz4cX648WvLNpYDv7WPzlFzIWm6cnQCbUToQ+uK3K4WyVuuqYtZqqI0f4wWUQ==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/atproto": "^3.1.7",
+
"@atcute/lexicons": "^1.2.2"
+
}
+
},
+
"node_modules/@atcute/car": {
+
"version": "3.1.2",
+
"resolved": "https://registry.npmjs.org/@atcute/car/-/car-3.1.2.tgz",
+
"integrity": "sha512-OZoi1C20Nj8aDRM/A5JeeQMLsQRm6/B7PqVI7T2tyoojiBsL+Vm42QRKxtTsJg+VFaTnWhOzQbf08GZpf2YW4Q==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/cbor": "^2.2.6",
+
"@atcute/cid": "^2.2.4",
+
"@atcute/uint8array": "^1.0.5",
+
"@atcute/varint": "^1.0.3",
+
"yocto-queue": "^1.2.1"
+
}
+
},
+
"node_modules/@atcute/cbor": {
+
"version": "2.2.6",
+
"resolved": "https://registry.npmjs.org/@atcute/cbor/-/cbor-2.2.6.tgz",
+
"integrity": "sha512-pDfsn/vPTmgeXZiZdyc5vCGCPSxWlfTUIGFMCd5SroAgoLk1v9xxF7R/8+gt1lj1OKAwCwhS0doVmtLjqqzdbA==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/cid": "^2.2.4",
+
"@atcute/multibase": "^1.1.6",
+
"@atcute/uint8array": "^1.0.5"
+
}
+
},
+
"node_modules/@atcute/cid": {
+
"version": "2.2.4",
+
"resolved": "https://registry.npmjs.org/@atcute/cid/-/cid-2.2.4.tgz",
+
"integrity": "sha512-6RUMyt7rp6KOSb4TWwifOZURnFrGgKqYyjVkYjiAcscZWgJpJxwoCUCdonxCfxhQtB0yJ+WlfqNXicGB+Pe94A==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/multibase": "^1.1.6",
+
"@atcute/uint8array": "^1.0.5"
+
}
+
},
+
"node_modules/@atcute/client": {
+
"version": "4.0.4",
+
"resolved": "https://registry.npmjs.org/@atcute/client/-/client-4.0.4.tgz",
+
"integrity": "sha512-0vkYe6HcGAef8FS4dlGMqCCPG4I4Lve1R8Amk8UEviUVofiqlv1WGoeez9CJFL8G/7vhcgVV9rPTHLJEjZ4RdQ==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/identity": "^1.1.1",
+
"@atcute/lexicons": "^1.2.2"
+
}
+
},
+
"node_modules/@atcute/identity": {
+
"version": "1.1.1",
+
"resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.1.1.tgz",
+
"integrity": "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q==",
+
"license": "0BSD",
+
"peer": true,
+
"dependencies": {
+
"@atcute/lexicons": "^1.2.2",
+
"@badrap/valita": "^0.4.6"
+
}
+
},
+
"node_modules/@atcute/identity-resolver": {
+
"version": "1.1.4",
+
"resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.1.4.tgz",
+
"integrity": "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/lexicons": "^1.2.2",
+
"@atcute/util-fetch": "^1.0.3",
+
"@badrap/valita": "^0.4.6"
+
},
+
"peerDependencies": {
+
"@atcute/identity": "^1.0.0"
+
}
+
},
+
"node_modules/@atcute/lex-cli": {
+
"version": "2.2.2",
+
"resolved": "https://registry.npmjs.org/@atcute/lex-cli/-/lex-cli-2.2.2.tgz",
+
"integrity": "sha512-5hScXu4i01WNLkmMmLtQgyOBwZh9M4nijhJ9BZExA+d33/rGlJ4Us1oclw/rbEWPAjqkhA38t30KGvOfKr3chw==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/lexicon-doc": "^1.1.2",
+
"@badrap/valita": "^0.4.6",
+
"@externdefs/collider": "^0.3.0",
+
"picocolors": "^1.1.1",
+
"prettier": "^3.6.2"
+
},
+
"bin": {
+
"lex-cli": "cli.mjs"
+
}
+
},
+
"node_modules/@atcute/lexicon-doc": {
+
"version": "1.1.2",
+
"resolved": "https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-1.1.2.tgz",
+
"integrity": "sha512-Q3ONR2635MTVWT5Fi01FFcYTfciav0ATnX5ZBon7160hiDyk4n1a9dl8dQYgx+st2/IB0ZCNvOMHPCMZacdktg==",
+
"license": "0BSD",
+
"dependencies": {
+
"@badrap/valita": "^0.4.6"
+
}
+
},
+
"node_modules/@atcute/lexicons": {
+
"version": "1.2.2",
+
"resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.2.tgz",
+
"integrity": "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA==",
+
"license": "0BSD",
+
"dependencies": {
+
"@standard-schema/spec": "^1.0.0",
+
"esm-env": "^1.2.2"
+
}
+
},
+
"node_modules/@atcute/multibase": {
+
"version": "1.1.6",
+
"resolved": "https://registry.npmjs.org/@atcute/multibase/-/multibase-1.1.6.tgz",
+
"integrity": "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg==",
+
"license": "0BSD",
+
"dependencies": {
+
"@atcute/uint8array": "^1.0.5"
+
}
+
},
+
"node_modules/@atcute/uint8array": {
+
"version": "1.0.5",
+
"resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.0.5.tgz",
+
"integrity": "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q==",
+
"license": "0BSD"
+
},
+
"node_modules/@atcute/util-fetch": {
+
"version": "1.0.3",
+
"resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.3.tgz",
+
"integrity": "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ==",
+
"license": "0BSD",
+
"dependencies": {
+
"@badrap/valita": "^0.4.6"
+
}
+
},
+
"node_modules/@atcute/varint": {
+
"version": "1.0.3",
+
"resolved": "https://registry.npmjs.org/@atcute/varint/-/varint-1.0.3.tgz",
+
"integrity": "sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog==",
+
"license": "0BSD"
+
},
+
"node_modules/@badrap/valita": {
+
"version": "0.4.6",
+
"resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz",
+
"integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==",
+
"license": "MIT",
+
"peer": true,
+
"engines": {
+
"node": ">= 18"
+
}
+
},
+
"node_modules/@externdefs/collider": {
+
"version": "0.3.0",
+
"resolved": "https://registry.npmjs.org/@externdefs/collider/-/collider-0.3.0.tgz",
+
"integrity": "sha512-x5CpeZ4c8n+1wMFthUMWSQKqCGcQo52/Qbda5ES+JFRRg/D8Ep6/JOvUUq5HExFuv/wW+6UYG2U/mXzw0IAd8Q==",
+
"license": "MIT",
+
"peerDependencies": {
+
"@badrap/valita": "^0.4.4"
+
}
+
},
+
"node_modules/@skyware/jetstream": {
+
"version": "0.2.5",
+
"resolved": "https://registry.npmjs.org/@skyware/jetstream/-/jetstream-0.2.5.tgz",
+
"integrity": "sha512-fM/zs03DLwqRyzZZJFWN20e76KrdqIp97Tlm8Cek+vxn96+tu5d/fx79V6H85L0QN6HvGiX2l9A8hWFqHvYlOA==",
+
"license": "MPL-2.0",
+
"dependencies": {
+
"@atcute/atproto": "^3.1.0",
+
"@atcute/bluesky": "^3.1.4",
+
"@atcute/lexicons": "^1.1.0",
+
"partysocket": "^1.1.3",
+
"tiny-emitter": "^2.1.0"
+
}
+
},
+
"node_modules/@standard-schema/spec": {
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+
"license": "MIT"
+
},
+
"node_modules/@types/better-sqlite3": {
+
"version": "7.6.13",
+
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"@types/node": "*"
+
}
+
},
+
"node_modules/@types/node": {
+
"version": "24.7.1",
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz",
+
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
+
"dev": true,
+
"license": "MIT",
+
"dependencies": {
+
"undici-types": "~7.14.0"
+
}
+
},
+
"node_modules/base64-js": {
+
"version": "1.5.1",
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "MIT"
+
},
+
"node_modules/better-sqlite3": {
+
"version": "12.4.1",
+
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
+
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
+
"hasInstallScript": true,
+
"license": "MIT",
+
"dependencies": {
+
"bindings": "^1.5.0",
+
"prebuild-install": "^7.1.1"
+
},
+
"engines": {
+
"node": "20.x || 22.x || 23.x || 24.x"
+
}
+
},
+
"node_modules/bindings": {
+
"version": "1.5.0",
+
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+
"license": "MIT",
+
"dependencies": {
+
"file-uri-to-path": "1.0.0"
+
}
+
},
+
"node_modules/bl": {
+
"version": "4.1.0",
+
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+
"license": "MIT",
+
"dependencies": {
+
"buffer": "^5.5.0",
+
"inherits": "^2.0.4",
+
"readable-stream": "^3.4.0"
+
}
+
},
+
"node_modules/buffer": {
+
"version": "5.7.1",
+
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "MIT",
+
"dependencies": {
+
"base64-js": "^1.3.1",
+
"ieee754": "^1.1.13"
+
}
+
},
+
"node_modules/chownr": {
+
"version": "1.1.4",
+
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+
"license": "ISC"
+
},
+
"node_modules/decompress-response": {
+
"version": "6.0.0",
+
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+
"license": "MIT",
+
"dependencies": {
+
"mimic-response": "^3.1.0"
+
},
+
"engines": {
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
+
"node_modules/deep-extend": {
+
"version": "0.6.0",
+
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+
"license": "MIT",
+
"engines": {
+
"node": ">=4.0.0"
+
}
+
},
+
"node_modules/detect-libc": {
+
"version": "2.1.2",
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+
"license": "Apache-2.0",
+
"engines": {
+
"node": ">=8"
+
}
+
},
+
"node_modules/dotenv": {
+
"version": "17.2.3",
+
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+
"license": "BSD-2-Clause",
+
"engines": {
+
"node": ">=12"
+
},
+
"funding": {
+
"url": "https://dotenvx.com"
+
}
+
},
+
"node_modules/end-of-stream": {
+
"version": "1.4.5",
+
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+
"license": "MIT",
+
"dependencies": {
+
"once": "^1.4.0"
+
}
+
},
+
"node_modules/esm-env": {
+
"version": "1.2.2",
+
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+
"license": "MIT"
+
},
+
"node_modules/event-target-polyfill": {
+
"version": "0.0.4",
+
"resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz",
+
"integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==",
+
"license": "MIT"
+
},
+
"node_modules/expand-template": {
+
"version": "2.0.3",
+
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+
"license": "(MIT OR WTFPL)",
+
"engines": {
+
"node": ">=6"
+
}
+
},
+
"node_modules/file-uri-to-path": {
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+
"license": "MIT"
+
},
+
"node_modules/fs-constants": {
+
"version": "1.0.0",
+
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+
"license": "MIT"
+
},
+
"node_modules/github-from-package": {
+
"version": "0.0.0",
+
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+
"license": "MIT"
+
},
+
"node_modules/ieee754": {
+
"version": "1.2.1",
+
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "BSD-3-Clause"
+
},
+
"node_modules/inherits": {
+
"version": "2.0.4",
+
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+
"license": "ISC"
+
},
+
"node_modules/ini": {
+
"version": "1.3.8",
+
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+
"license": "ISC"
+
},
+
"node_modules/kysely": {
+
"version": "0.28.7",
+
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.7.tgz",
+
"integrity": "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw==",
+
"license": "MIT",
+
"engines": {
+
"node": ">=20.0.0"
+
}
+
},
+
"node_modules/mimic-response": {
+
"version": "3.1.0",
+
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+
"license": "MIT",
+
"engines": {
+
"node": ">=10"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
},
+
"node_modules/minimist": {
+
"version": "1.2.8",
+
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+
"license": "MIT",
+
"funding": {
+
"url": "https://github.com/sponsors/ljharb"
+
}
+
},
+
"node_modules/mkdirp-classic": {
+
"version": "0.5.3",
+
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+
"license": "MIT"
+
},
+
"node_modules/napi-build-utils": {
+
"version": "2.0.0",
+
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+
"license": "MIT"
+
},
+
"node_modules/node-abi": {
+
"version": "3.78.0",
+
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
+
"integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
+
"license": "MIT",
+
"dependencies": {
+
"semver": "^7.3.5"
+
},
+
"engines": {
+
"node": ">=10"
+
}
+
},
+
"node_modules/once": {
+
"version": "1.4.0",
+
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+
"license": "ISC",
+
"dependencies": {
+
"wrappy": "1"
+
}
+
},
+
"node_modules/partysocket": {
+
"version": "1.1.6",
+
"resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.6.tgz",
+
"integrity": "sha512-LkEk8N9hMDDsDT0iDK0zuwUDFVrVMUXFXCeN3850Ng8wtjPqPBeJlwdeY6ROlJSEh3tPoTTasXoSBYH76y118w==",
+
"license": "MIT",
+
"dependencies": {
+
"event-target-polyfill": "^0.0.4"
+
}
+
},
+
"node_modules/picocolors": {
+
"version": "1.1.1",
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+
"license": "ISC"
+
},
+
"node_modules/prebuild-install": {
+
"version": "7.1.3",
+
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+
"license": "MIT",
+
"dependencies": {
+
"detect-libc": "^2.0.0",
+
"expand-template": "^2.0.3",
+
"github-from-package": "0.0.0",
+
"minimist": "^1.2.3",
+
"mkdirp-classic": "^0.5.3",
+
"napi-build-utils": "^2.0.0",
+
"node-abi": "^3.3.0",
+
"pump": "^3.0.0",
+
"rc": "^1.2.7",
+
"simple-get": "^4.0.0",
+
"tar-fs": "^2.0.0",
+
"tunnel-agent": "^0.6.0"
+
},
+
"bin": {
+
"prebuild-install": "bin.js"
+
},
+
"engines": {
+
"node": ">=10"
+
}
+
},
+
"node_modules/prettier": {
+
"version": "3.6.2",
+
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+
"license": "MIT",
+
"bin": {
+
"prettier": "bin/prettier.cjs"
+
},
+
"engines": {
+
"node": ">=14"
+
},
+
"funding": {
+
"url": "https://github.com/prettier/prettier?sponsor=1"
+
}
+
},
+
"node_modules/pump": {
+
"version": "3.0.3",
+
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+
"license": "MIT",
+
"dependencies": {
+
"end-of-stream": "^1.1.0",
+
"once": "^1.3.1"
+
}
+
},
+
"node_modules/rc": {
+
"version": "1.2.8",
+
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+
"dependencies": {
+
"deep-extend": "^0.6.0",
+
"ini": "~1.3.0",
+
"minimist": "^1.2.0",
+
"strip-json-comments": "~2.0.1"
+
},
+
"bin": {
+
"rc": "cli.js"
+
}
+
},
+
"node_modules/readable-stream": {
+
"version": "3.6.2",
+
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+
"license": "MIT",
+
"dependencies": {
+
"inherits": "^2.0.3",
+
"string_decoder": "^1.1.1",
+
"util-deprecate": "^1.0.1"
+
},
+
"engines": {
+
"node": ">= 6"
+
}
+
},
+
"node_modules/safe-buffer": {
+
"version": "5.2.1",
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "MIT"
+
},
+
"node_modules/semver": {
+
"version": "7.7.3",
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+
"license": "ISC",
+
"bin": {
+
"semver": "bin/semver.js"
+
},
+
"engines": {
+
"node": ">=10"
+
}
+
},
+
"node_modules/simple-concat": {
+
"version": "1.0.1",
+
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "MIT"
+
},
+
"node_modules/simple-get": {
+
"version": "4.0.1",
+
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+
"funding": [
+
{
+
"type": "github",
+
"url": "https://github.com/sponsors/feross"
+
},
+
{
+
"type": "patreon",
+
"url": "https://www.patreon.com/feross"
+
},
+
{
+
"type": "consulting",
+
"url": "https://feross.org/support"
+
}
+
],
+
"license": "MIT",
+
"dependencies": {
+
"decompress-response": "^6.0.0",
+
"once": "^1.3.1",
+
"simple-concat": "^1.0.0"
+
}
+
},
+
"node_modules/string_decoder": {
+
"version": "1.3.0",
+
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+
"license": "MIT",
+
"dependencies": {
+
"safe-buffer": "~5.2.0"
+
}
+
},
+
"node_modules/strip-json-comments": {
+
"version": "2.0.1",
+
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+
"license": "MIT",
+
"engines": {
+
"node": ">=0.10.0"
+
}
+
},
+
"node_modules/tar-fs": {
+
"version": "2.1.4",
+
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+
"license": "MIT",
+
"dependencies": {
+
"chownr": "^1.1.1",
+
"mkdirp-classic": "^0.5.2",
+
"pump": "^3.0.0",
+
"tar-stream": "^2.1.4"
+
}
+
},
+
"node_modules/tar-stream": {
+
"version": "2.2.0",
+
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+
"license": "MIT",
+
"dependencies": {
+
"bl": "^4.0.3",
+
"end-of-stream": "^1.4.1",
+
"fs-constants": "^1.0.0",
+
"inherits": "^2.0.3",
+
"readable-stream": "^3.1.1"
+
},
+
"engines": {
+
"node": ">=6"
+
}
+
},
+
"node_modules/tiny-emitter": {
+
"version": "2.1.0",
+
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+
"license": "MIT"
+
},
+
"node_modules/tunnel-agent": {
+
"version": "0.6.0",
+
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+
"license": "Apache-2.0",
+
"dependencies": {
+
"safe-buffer": "^5.0.1"
+
},
+
"engines": {
+
"node": "*"
+
}
+
},
+
"node_modules/undici-types": {
+
"version": "7.14.0",
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+
"dev": true,
+
"license": "MIT"
+
},
+
"node_modules/util-deprecate": {
+
"version": "1.0.2",
+
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+
"license": "MIT"
+
},
+
"node_modules/wrappy": {
+
"version": "1.0.2",
+
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+
"license": "ISC"
+
},
+
"node_modules/yocto-queue": {
+
"version": "1.2.1",
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
+
"integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
+
"license": "MIT",
+
"engines": {
+
"node": ">=12.20"
+
},
+
"funding": {
+
"url": "https://github.com/sponsors/sindresorhus"
+
}
+
}
+
}
+
}
+29
package.json
···
+
{
+
"name": "mewsse-links",
+
"version": "0.0.1",
+
"description": "Just a cutom appview for my links",
+
"main": "src/index.ts",
+
"type": "module",
+
"scripts": {
+
"start": "node src/index.ts"
+
},
+
"author": "Mewsse",
+
"license": "WTFPL",
+
"dependencies": {
+
"@atcute/atproto": "^3.1.7",
+
"@atcute/car": "^3.1.2",
+
"@atcute/cbor": "^2.2.6",
+
"@atcute/client": "^4.0.4",
+
"@atcute/identity-resolver": "^1.1.4",
+
"@atcute/lex-cli": "^2.2.2",
+
"@atcute/lexicons": "^1.2.2",
+
"@skyware/jetstream": "^0.2.5",
+
"better-sqlite3": "^12.4.1",
+
"dotenv": "^17.2.3",
+
"kysely": "^0.28.7"
+
},
+
"devDependencies": {
+
"@types/better-sqlite3": "^7.6.13",
+
"@types/node": "^24.7.1"
+
}
+
}
+62
src/db.ts
···
+
import { Kysely, Migrator, SqliteDialect } from 'kysely'
+
import SqliteDb from 'better-sqlite3'
+
+
import type { Migration, MigrationProvider } from 'kysely'
+
+
export type DatabaseSchema = {
+
links: Link,
+
}
+
+
export type Link = {
+
rkey: string,
+
link: string,
+
title: string,
+
description: string | null,
+
tag: string | null,
+
image: string | null,
+
alt: string | null,
+
createdAt: string,
+
}
+
+
const migrations: Record<string, Migration> = {}
+
const migrationProvider: MigrationProvider = {
+
async getMigrations() {
+
return migrations
+
},
+
}
+
+
migrations['001'] = {
+
async up(db: Kysely<any>) {
+
await db.schema
+
.createTable('links')
+
.addColumn('rkey', 'varchar', (col) => col.primaryKey())
+
.addColumn('link', 'varchar', (col) => col.notNull())
+
.addColumn('title', 'varchar', (col) => col.notNull())
+
.addColumn('description', 'varchar')
+
.addColumn('tag', 'varchar')
+
.addColumn('image', 'varchar')
+
.addColumn('alt', 'varchar')
+
.addColumn('createdAt', 'varchar', (col) => col.notNull())
+
.execute()
+
},
+
+
async down(db: Kysely<any>) {
+
await db.schema.dropTable('links').execute()
+
},
+
}
+
+
export const createDb = (location: string): Database => {
+
return new Kysely<DatabaseSchema>({
+
dialect: new SqliteDialect({
+
database: new SqliteDb(location)
+
}),
+
})
+
}
+
+
export const migrateToLatest = async (db: Database) => {
+
const migrator = new Migrator({db, provider: migrationProvider})
+
const { error } = await migrator.migrateToLatest()
+
if(error) throw error
+
}
+
+
export type Database = Kysely<DatabaseSchema>
+76
src/id-resolver.ts
···
+
import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'
+
import { isDid} from '@atcute/lexicons/syntax'
+
import process from 'process'
+
+
import type { DidDocument, Service } from '@atcute/identity'
+
import type { Did } from '@atcute/lexicons/syntax'
+
+
const docResolver = new CompositeDidDocumentResolver({
+
methods: {
+
plc: new PlcDidDocumentResolver(),
+
web: new WebDidDocumentResolver()
+
}
+
})
+
+
export class DIDError extends Error {
+
constructor(msg: string) {
+
super(msg)
+
+
Object.setPrototypeOf(this, DIDError.prototype)
+
}
+
}
+
+
export class ServiceError extends Error {
+
constructor(msg: string) {
+
super(msg)
+
+
Object.setPrototypeOf(this, ServiceError.prototype)
+
}
+
}
+
+
export function getUserDID() : Did<"web"> | Did<"plc"> {
+
const did = process.env.DID
+
+
if (!did || did == "") {
+
throw new DIDError("Missing DID to ingest")
+
}
+
+
if (!isDid(did)) {
+
throw new DIDError("DID is not in the correct format")
+
}
+
+
return did as Did<"web"> | Did<"plc">
+
}
+
+
export async function findUserDIDDoc() : Promise<DidDocument> {
+
+
try {
+
const did = getUserDID()
+
const doc = await docResolver.resolve(did)
+
return doc
+
} catch (err) {
+
throw err
+
}
+
}
+
+
export async function findUserPDS(): Promise<string> {
+
const didDoc = await findUserDIDDoc()
+
+
if (!didDoc.service) {
+
throw new ServiceError("No service found on user did doc")
+
}
+
+
const pds = didDoc.service.filter(service => service.id == "#atproto_pds")
+
+
if (pds.length < 1) {
+
throw new ServiceError(`No valid service found for ${process.env.DID}`)
+
}
+
+
let serviceEndpoint = pds.shift()?.serviceEndpoint
+
+
if (!serviceEndpoint || typeof serviceEndpoint != 'string' ) {
+
throw new ServiceError(`No valid service found for ${process.env.DID}`)
+
}
+
+
return serviceEndpoint
+
}
+69
src/index.ts
···
+
import { createDb, migrateToLatest } from './db.ts'
+
import { createIngester } from './ingester.ts'
+
import dotenv from 'dotenv'
+
import process from 'process'
+
+
import type { Database } from './db.ts'
+
import { Jetstream } from '@skyware/jetstream'
+
import { logger } from './lib/logger.ts'
+
+
dotenv.config()
+
+
export type Context = {
+
db: Database
+
jetstream: Jetstream
+
}
+
+
export class Server {
+
public ctx: Context
+
+
constructor(
+
ctx: Context
+
) {
+
this.ctx = ctx
+
}
+
+
static async create() {
+
const db = createDb(process.env.DB_PATH || ":memory")
+
await migrateToLatest(db)
+
const ingester = createIngester(db)
+
+
await ingester.backfill()
+
const jetstream = await ingester.jetstream()
+
+
const ctx: Context= {
+
db,
+
jetstream
+
}
+
+
jetstream.start()
+
logger.info("Starting jetstream client")
+
+
return new Server(ctx)
+
}
+
+
async close() {
+
logger.info("Stopping jetstream client")
+
await this.ctx.jetstream.close()
+
+
return new Promise<void>((resolve) => {
+
resolve()
+
})
+
}
+
}
+
+
+
const run = async () => {
+
const server = await Server.create()
+
+
const onClose = async () => {
+
setTimeout(() => process.exit(1), 10000).unref()
+
await server.close()
+
process.exit()
+
}
+
+
process.on('SIGINT', onClose)
+
process.on('SIGTERM', onClose)
+
}
+
+
run()
+175
src/ingester.ts
···
+
import type { Records as _Records } from "@atcute/lexicons/ambient"
+
import type { Did } from '@atcute/lexicons/syntax'
+
import type { Database, Link } from './db.ts'
+
+
import { Client, simpleFetchHandler } from '@atcute/client'
+
import { Jetstream } from '@skyware/jetstream'
+
import { findUserPDS, getUserDID } from './id-resolver.ts'
+
import { RepoReader } from '@atcute/car/v4'
+
import { decode } from '@atcute/cbor'
+
import { logger } from "./lib/logger.ts"
+
+
interface RepoParams {
+
did: Did<"web"> | Did<"plc">,
+
since?: string
+
}
+
+
export class IngestionError extends Error {
+
constructor(msg: string) {
+
super(msg)
+
+
Object.setPrototypeOf(this, IngestionError.prototype)
+
}
+
}
+
+
export function findImage(did: Did<"web"> | Did<"plc">, pds: string, record: any): string | null {
+
const imageCid = record.image ? record.image.ref.$link : null
+
if (!imageCid) return null
+
+
// let the user pull the blob with their browser directly fomr the pds
+
// decreasing space needed to run the service and prevent duplication
+
// if hosted at the same place as the pds (self host anyone ?)
+
return `${pds}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${imageCid}`
+
}
+
+
export function createIngester(db: Database) {
+
return {
+
async backfill() : Promise<any> {
+
const did = getUserDID()
+
const pds = await findUserPDS()
+
const handler = simpleFetchHandler({service: pds})
+
const rpc = new Client({ handler })
+
const now = new Date()
+
+
logger.info(`Starting backfilling`)
+
+
const params: RepoParams = {
+
did
+
}
+
+
const lastRev = await db
+
.selectFrom('links')
+
.select('rkey')
+
.limit(1)
+
.executeTakeFirst()
+
+
if (lastRev) {
+
params.since = lastRev.rkey
+
}
+
+
const {ok, data} = await rpc.get(`com.atproto.sync.getRepo`, {
+
params,
+
as: 'stream'
+
})
+
+
if (!ok) {
+
throw new IngestionError(`Error while syncing repo for ${did} on ${pds}`)
+
}
+
+
await using repo = RepoReader.fromStream(data)
+
+
for await (const entry of repo) {
+
if (entry.collection != "pet.mewsse.link") continue
+
const link : Link = decode(entry.bytes)
+
+
await db
+
.insertInto('links')
+
.values({
+
rkey: entry.rkey,
+
link: link.link,
+
title: link.title,
+
description: link.description,
+
tag: link.tag ?? null,
+
image: findImage(did, pds, link),
+
alt: link.alt ?? null,
+
createdAt: link.createdAt
+
})
+
.onConflict((conflict) =>
+
conflict.column('rkey').doUpdateSet({
+
link: link.link,
+
title: link.title,
+
description: link.description,
+
tag: link.tag ?? null,
+
image: findImage(did, pds, link),
+
alt: link.alt ?? null,
+
})
+
)
+
.execute()
+
+
logger.info(`Inserting record ${entry.rkey}`)
+
}
+
+
logger.info(`Backfilling ended`)
+
},
+
+
async jetstream() : Promise<Jetstream> {
+
const did = getUserDID()
+
const pds = await findUserPDS()
+
+
const jetstream = new Jetstream({
+
wantedCollections: ['pet.mewsse.link'],
+
wantedDids: [did]
+
})
+
+
jetstream.onCreate('pet.mewsse.link', async (event) => {
+
if (event.commit.record.$type != "pet.mewsse.link") return
+
+
const rev = event.commit.rev
+
const record = event.commit.record
+
+
+
await db
+
.insertInto('links')
+
.values({
+
rkey: rev,
+
link: record.link,
+
title: record.title,
+
description: record.description ?? null,
+
tag: record.tag ?? null,
+
image: findImage(did, pds, record),
+
alt: record.alt ?? null,
+
createdAt: record.createdAt
+
})
+
.execute()
+
+
logger.info(`Record ${rev} created`)
+
})
+
+
jetstream.onUpdate('pet.mewsse.link', async (event) => {
+
if (event.commit.record.$type != "pet.mewsse.link") return
+
+
const rev = event.commit.rev
+
const record = event.commit.record
+
+
await db
+
.updateTable('links')
+
.set({
+
link: record.link,
+
title: record.title,
+
description: record.description ?? null,
+
tag: record.tag ?? null,
+
image: findImage(did, pds, record),
+
alt: record.alt ?? null,
+
createdAt: record.createdAt
+
})
+
.where('rkey', '=', rev)
+
.executeTakeFirstOrThrow()
+
+
logger.info(`Record ${rev} updated`)
+
})
+
+
jetstream.onDelete('pet.mewsse.link', async (event) => {
+
if (event.commit.collection != "pet.mewsse.link") return
+
+
await db
+
.deleteFrom('links')
+
.where('rkey', '=', event.commit.rkey)
+
.executeTakeFirstOrThrow()
+
+
logger.info(`Record ${event.commit.rkey} deleted`)
+
})
+
+
return jetstream
+
}
+
}
+
}
+1
src/lexicons/index.ts
···
+
export * as PetMewsseLink from "./types/pet/mewsse/link.js"
+65
src/lexicons/types/pet/mewsse/link.ts
···
+
import type {} from "@atcute/lexicons"
+
import * as v from "@atcute/lexicons/validations"
+
import type {} from "@atcute/lexicons/ambient"
+
+
const _mainSchema = /*#__PURE__*/ v.record(
+
/*#__PURE__*/ v.tidString(),
+
/*#__PURE__*/ v.object({
+
$type: /*#__PURE__*/ v.literal("pet.mewsse.link"),
+
/**
+
* A alt text to describe the image.
+
* @maxLength 3000
+
*/
+
alt: /*#__PURE__*/ v.optional(
+
/*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [
+
/*#__PURE__*/ v.stringLength(0, 3000),
+
]),
+
),
+
/**
+
* Client-declared timestamp when this post was originally created.
+
*/
+
createdAt: /*#__PURE__*/ v.datetimeString(),
+
/**
+
* Short description for the content of the link.
+
*/
+
description: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
+
/**
+
* An image to illustrate the link
+
* @accept image/*
+
* @maxSize 1000000
+
*/
+
image: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.blob()),
+
/**
+
* The link to point to.
+
* @maxLength 3000
+
*/
+
link: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [
+
/*#__PURE__*/ v.stringLength(0, 3000),
+
]),
+
/**
+
* A tag for classify the link.
+
*/
+
tag: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()),
+
/**
+
* Title of the given link.
+
* @maxLength 3000
+
*/
+
title: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [
+
/*#__PURE__*/ v.stringLength(0, 3000),
+
]),
+
}),
+
)
+
+
type main$schematype = typeof _mainSchema
+
+
export interface mainSchema extends main$schematype {}
+
+
export const mainSchema = _mainSchema as mainSchema
+
+
export interface Main extends v.InferInput<typeof mainSchema> {}
+
+
declare module "@atcute/lexicons/ambient" {
+
interface Records {
+
"pet.mewsse.link": mainSchema
+
}
+
}
+47
src/lib/logger.ts
···
+
interface logLevelgInterface {
+
[key:string]: number
+
}
+
+
+
export const logger = {
+
debug: logDebug,
+
info: logInfo,
+
warn: logWarn,
+
error: logError,
+
fatal: logFatal
+
}
+
+
export const logLevel:logLevelgInterface = {
+
DEBUG: 5,
+
INFO: 4,
+
WARN: 3,
+
ERROR: 2,
+
FATAL: 1,
+
} as const
+
+
export function log(level: number, type: string, message: string): void {
+
const envLevel = process.env.LOG_LEVEL ? logLevel[process.env.LOG_LEVEL] : logLevel.INFO
+
if (level > envLevel) return
+
+
console.log(`${type} ${new Date().toISOString()}: ${message}`)
+
}
+
+
function logDebug(message: string): void {
+
log(logLevel.DEBUG, "[DEBUG]", message)
+
}
+
+
function logInfo(message: string): void {
+
log(logLevel.INFO, "[INFO]", message)
+
}
+
+
function logWarn(message: string): void {
+
log(logLevel.WARN, "[WARN]", message)
+
}
+
+
function logError(message: string): void {
+
log(logLevel.ERROR, "[WARN]", message)
+
}
+
+
function logFatal(message: string): void {
+
log(logLevel.ERROR, "[WARN]", message)
+
}
+29
tsconfig.json
···
+
{
+
"compilerOptions": {
+
"baseUrl": ".",
+
"outDir": "dist",
+
"importsNotUsedAsValues": "remove",
+
"forceConsistentCasingInFileNames": true,
+
"allowImportingTsExtensions": true,
+
"rewriteRelativeImportExtensions": true,
+
+
"lib": [
+
"es2024",
+
"ESNext.Array",
+
"ESNext.Collection",
+
"ESNext.Iterator",
+
"ESNext.Promise",
+
"DOM"
+
],
+
"types": ["node"],
+
"module": "nodenext",
+
"target": "es2024",
+
+
"strict": true,
+
"esModuleInterop": true,
+
"skipLibCheck": true,
+
"moduleResolution": "node16"
+
},
+
"include": ["src/**/*"],
+
"exclude": ["node_modules"]
+
}