tangled
alpha
login
or
join now
oppi.li
/
plonkli
21
fork
atom
atproto pastebin service: https://plonk.li
21
fork
atom
overview
issues
pulls
pipelines
stuff
oppi.li
1 year ago
12f82259
4b848da2
+690
-55
17 changed files
expand all
collapse all
unified
split
flake.nix
lexicons
paste.json
src
db.ts
ingester.ts
lexicons
index.ts
lexicons.ts
types
com
atproto
repo
getRecord.ts
listRecords.ts
ovh
plonk
comment.ts
paste.ts
mixins
post.pug
public
styles.css
routes.ts
views
index.pug
paste.pug
user.pug
tsup.config.ts
+23
-1
flake.nix
···
10
supportedSystems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"];
11
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
12
nixpkgsFor = forAllSystems (system:
13
-
import nixpkgs { inherit system; });
0
0
0
14
in {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
15
16
devShell = forAllSystems (system: let
17
pkgs = nixpkgsFor."${system}";
···
10
supportedSystems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"];
11
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
12
nixpkgsFor = forAllSystems (system:
13
+
import nixpkgs {
14
+
inherit system;
15
+
overlays = [self.overlay.default];
16
+
});
17
in {
18
+
overlay.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
+
packageJson = ./package.json;
27
+
buildPhase = "npm run build";
28
+
npmDepsHash = "sha256-qGCbaFAHd/s9hOTWMjHCam6Kf6pU6IWPybfwYh0sOwc=";
29
+
};
30
+
};
31
+
32
+
packages = forAllSystems (system: {
33
+
inherit (nixpkgsFor."${system}") plonk;
34
+
});
35
+
36
+
defaultPackage = forAllSystems (system: nixpkgsFor."${system}".plonk);
37
38
devShell = forAllSystems (system: let
39
pkgs = nixpkgsFor."${system}";
+17
-2
lexicons/paste.json
···
7
"key": "tid",
8
"record": {
9
"type": "object",
10
-
"required": ["code", "lang", "title", "createdAt"],
0
0
0
0
0
0
11
"properties": {
12
"code": {
13
"type": "string",
···
15
"maxGraphemes": 65536,
16
"maxLength": 65536
17
},
0
0
0
0
0
0
18
"lang": {
19
"type": "string",
20
"minLength": 1,
···
27
"maxGraphemes": 100,
28
"maxLength": 100
29
},
30
-
"createdAt": { "type": "string", "format": "datetime" }
0
0
0
31
}
32
}
33
}
···
7
"key": "tid",
8
"record": {
9
"type": "object",
10
+
"required": [
11
+
"code",
12
+
"shortUrl",
13
+
"lang",
14
+
"title",
15
+
"createdAt"
16
+
],
17
"properties": {
18
"code": {
19
"type": "string",
···
21
"maxGraphemes": 65536,
22
"maxLength": 65536
23
},
24
+
"shortUrl": {
25
+
"type": "string",
26
+
"minLength": 2,
27
+
"maxGraphemes": 10,
28
+
"maxLength": 10
29
+
},
30
"lang": {
31
"type": "string",
32
"minLength": 1,
···
39
"maxGraphemes": 100,
40
"maxLength": 100
41
},
42
+
"createdAt": {
43
+
"type": "string",
44
+
"format": "datetime"
45
+
}
46
}
47
}
48
}
+31
src/db.ts
···
1
import SqliteDb from "better-sqlite3";
2
import { randomBytes } from "crypto";
0
3
4
import {
5
Kysely,
···
12
13
export type DatabaseSchema = {
14
paste: Paste;
0
15
auth_state: AuthState;
16
auth_session: AuthSession;
17
};
···
19
export type Paste = {
20
uri: string;
21
authorDid: string;
0
22
code: string;
23
lang: string;
24
title: string;
···
36
state: AuthStateJson;
37
};
38
0
0
0
0
0
0
0
0
0
0
39
type AuthSessionJson = string;
40
type AuthStateJson = string;
41
···
85
await db.schema.dropTable("paste").execute();
86
},
87
};
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
88
89
function generateShortString(length: number): string {
90
return randomBytes(length).toString("base64url").substring(0, length);
···
1
import SqliteDb from "better-sqlite3";
2
import { randomBytes } from "crypto";
3
+
import e from "express";
4
5
import {
6
Kysely,
···
13
14
export type DatabaseSchema = {
15
paste: Paste;
16
+
comment: Comment;
17
auth_state: AuthState;
18
auth_session: AuthSession;
19
};
···
21
export type Paste = {
22
uri: string;
23
authorDid: string;
24
+
shortUrl: string;
25
code: string;
26
lang: string;
27
title: string;
···
39
state: AuthStateJson;
40
};
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
+
52
type AuthSessionJson = string;
53
type AuthStateJson = string;
54
···
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();
117
+
},
118
+
}
119
120
function generateShortString(length: number): string {
121
return randomBytes(length).toString("base64url").substring(0, length);
+37
-5
src/ingester.ts
···
2
import { IdResolver } from "@atproto/identity";
3
import { Firehose } from "@atproto/sync";
4
import type { Database } from "#/db";
5
-
import { newShortUrl } from "#/db";
6
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
0
7
8
export function createIngester(db: Database, idResolver: IdResolver) {
9
const logger = pino({ name: "firehose ingestion" });
···
21
Paste.isRecord(record) &&
22
Paste.validateRecord(record).success
23
) {
24
-
// Store the status in our SQLite
25
-
const short_url = await newShortUrl(db);
26
await db
27
.insertInto("paste")
28
.values({
29
uri: evt.uri.toString(),
30
-
shortUrl,
31
authorDid: evt.did,
32
code: record.code,
33
lang: record.lang,
···
44
}),
45
)
46
.execute();
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
47
}
48
} else if (
49
evt.event === "delete" &&
···
54
.deleteFrom("paste")
55
.where("uri", "=", evt.uri.toString())
56
.execute();
57
-
}
0
0
0
0
0
0
0
0
0
58
},
59
onError: (err) => {
60
logger.error({ err }, "error on firehose ingestion");
···
2
import { IdResolver } from "@atproto/identity";
3
import { Firehose } from "@atproto/sync";
4
import type { Database } from "#/db";
0
5
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
6
+
import * as Comment from "#/lexicons/types/ovh/plonk/comment";
7
8
export function createIngester(db: Database, idResolver: IdResolver) {
9
const logger = pino({ name: "firehose ingestion" });
···
21
Paste.isRecord(record) &&
22
Paste.validateRecord(record).success
23
) {
0
0
24
await db
25
.insertInto("paste")
26
.values({
27
uri: evt.uri.toString(),
28
+
shortUrl: record.shortUrl,
29
authorDid: evt.did,
30
code: record.code,
31
lang: record.lang,
···
42
}),
43
)
44
.execute();
45
+
} else if (
46
+
evt.collection === "ovh.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();
70
}
71
} else if (
72
evt.event === "delete" &&
···
77
.deleteFrom("paste")
78
.where("uri", "=", evt.uri.toString())
79
.execute();
80
+
} else if (
81
+
evt.event === "delete" &&
82
+
evt.collection === "ovh.plonk.comment"
83
+
) {
84
+
// Remove the status from our SQLite
85
+
await db
86
+
.deleteFrom("comment")
87
+
.where("uri", "=", evt.uri.toString())
88
+
.execute();
89
+
}
90
},
91
onError: (err) => {
92
logger.error({ err }, "error on firehose ingestion");
+40
-16
src/lexicons/index.ts
···
9
StreamAuthVerifier,
10
} from '@atproto/xrpc-server'
11
import { schemas } from './lexicons'
0
0
12
13
export function createServer(options?: XrpcOptions): Server {
14
return new Server(options)
···
17
export class Server {
18
xrpc: XrpcServer
19
ovh: OvhNS
20
-
app: AppNS
21
com: ComNS
0
22
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)
0
28
}
29
}
30
···
46
}
47
}
48
49
-
export class AppNS {
50
_server: Server
51
-
bsky: AppBskyNS
52
53
constructor(server: Server) {
54
this._server = server
55
-
this.bsky = new AppBskyNS(server)
56
}
57
}
58
59
-
export class AppBskyNS {
60
_server: Server
61
-
actor: AppBskyActorNS
62
63
constructor(server: Server) {
64
this._server = server
65
-
this.actor = new AppBskyActorNS(server)
66
}
67
}
68
69
-
export class AppBskyActorNS {
70
_server: Server
71
72
constructor(server: Server) {
73
this._server = server
74
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
75
}
76
77
-
export class ComNS {
78
_server: Server
79
-
atproto: ComAtprotoNS
80
81
constructor(server: Server) {
82
this._server = server
83
-
this.atproto = new ComAtprotoNS(server)
84
}
85
}
86
87
-
export class ComAtprotoNS {
88
_server: Server
89
-
repo: ComAtprotoRepoNS
90
91
constructor(server: Server) {
92
this._server = server
93
-
this.repo = new ComAtprotoRepoNS(server)
94
}
95
}
96
97
-
export class ComAtprotoRepoNS {
98
_server: Server
99
100
constructor(server: Server) {
···
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'
14
15
export function createServer(options?: XrpcOptions): Server {
16
return new Server(options)
···
19
export class Server {
20
xrpc: XrpcServer
21
ovh: OvhNS
0
22
com: ComNS
23
+
app: AppNS
24
25
constructor(options?: XrpcOptions) {
26
this.xrpc = createXrpcServer(schemas, options)
27
this.ovh = new OvhNS(this)
0
28
this.com = new ComNS(this)
29
+
this.app = new AppNS(this)
30
}
31
}
32
···
48
}
49
}
50
51
+
export class ComNS {
52
_server: Server
53
+
atproto: ComAtprotoNS
54
55
constructor(server: Server) {
56
this._server = server
57
+
this.atproto = new ComAtprotoNS(server)
58
}
59
}
60
61
+
export class ComAtprotoNS {
62
_server: Server
63
+
repo: ComAtprotoRepoNS
64
65
constructor(server: Server) {
66
this._server = server
67
+
this.repo = new ComAtprotoRepoNS(server)
68
}
69
}
70
71
+
export class ComAtprotoRepoNS {
72
_server: Server
73
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
+
}
99
}
100
101
+
export class AppNS {
102
_server: Server
103
+
bsky: AppBskyNS
104
105
constructor(server: Server) {
106
this._server = server
107
+
this.bsky = new AppBskyNS(server)
108
}
109
}
110
111
+
export class AppBskyNS {
112
_server: Server
113
+
actor: AppBskyActorNS
114
115
constructor(server: Server) {
116
this._server = server
117
+
this.actor = new AppBskyActorNS(server)
118
}
119
}
120
121
+
export class AppBskyActorNS {
122
_server: Server
123
124
constructor(server: Server) {
+190
-1
src/lexicons/lexicons.ts
···
4
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
5
6
export const schemaDict = {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
7
ComAtprotoLabelDefs: {
8
lexicon: 1,
9
id: 'com.atproto.label.defs',
···
183
},
184
},
185
},
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
186
OvhPlonkPaste: {
187
lexicon: 1,
188
id: 'ovh.plonk.paste',
···
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,
0
0
0
0
0
0
202
},
203
lang: {
204
type: 'string',
···
301
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
302
export const lexicons: Lexicons = new Lexicons(schemas)
303
export const ids = {
0
304
ComAtprotoLabelDefs: 'com.atproto.label.defs',
0
0
305
OvhPlonkPaste: 'ovh.plonk.paste',
306
AppBskyActorProfile: 'app.bsky.actor.profile',
307
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
···
4
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
5
6
export const schemaDict = {
7
+
OvhPlonkComment: {
8
+
lexicon: 1,
9
+
id: 'ovh.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',
···
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
OvhPlonkPaste: {
367
lexicon: 1,
368
id: 'ovh.plonk.paste',
···
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',
···
487
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
488
export const lexicons: Lexicons = new Lexicons(schemas)
489
export const ids = {
490
+
OvhPlonkComment: 'ovh.plonk.comment',
491
ComAtprotoLabelDefs: 'com.atproto.label.defs',
492
+
ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',
493
+
ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',
494
OvhPlonkPaste: 'ovh.plonk.paste',
495
AppBskyActorProfile: 'app.bsky.actor.profile',
496
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
+55
src/lexicons/types/com/atproto/repo/getRecord.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}
+29
src/lexicons/types/ovh/plonk/comment.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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 === 'ovh.plonk.comment#main' || v.$type === 'ovh.plonk.comment')
24
+
)
25
+
}
26
+
27
+
export function validateRecord(v: unknown): ValidationResult {
28
+
return lexicons.validate('ovh.plonk.comment#main', v)
29
+
}
+1
src/lexicons/types/ovh/plonk/paste.ts
···
8
9
export interface Record {
10
code: string
0
11
lang: string
12
title: string
13
createdAt: string
···
8
9
export interface Record {
10
code: string
11
+
shortUrl: string
12
lang: string
13
title: string
14
createdAt: string
+3
-3
src/mixins/post.pug
···
1
-
mixin post(paste, didHandleMap)
2
div.post
3
p
4
a(href=`/p/${paste.shortUrl}`)
5
| #{paste.title}
6
p.post-info
7
| by
8
-
a(href=`/u/${encodeURIComponent(paste.authorDid)}`)
9
-
| @#{didHandleMap[paste.authorDid]}
10
| ·
11
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
12
| ·
···
1
+
mixin post(paste, handle, did)
2
div.post
3
p
4
a(href=`/p/${paste.shortUrl}`)
5
| #{paste.title}
6
p.post-info
7
| by
8
+
a(href=`/u/${did}`)
9
+
| @#{handle}
10
| ·
11
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
12
| ·
+1
-1
src/public/styles.css
···
127
align-self: flex-end;
128
}
129
130
-
.timeline {
131
display: flex;
132
flex-direction: column;
133
gap: 1rem;
···
127
align-self: flex-end;
128
}
129
130
+
.timeline, .comments {
131
display: flex;
132
flex-direction: column;
133
gap: 1rem;
+152
-23
src/routes.ts
···
8
import { Agent } from "@atproto/api";
9
import { getPds, DidResolver } from "@atproto/identity";
10
import { TID } from "@atproto/common";
0
11
import { newShortUrl } from "#/db";
12
13
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
0
0
14
15
type Session = {
16
did: string;
···
114
115
// Map user DIDs to their domain-name handles
116
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
117
-
pastes.map((s) => s.authorDid),
118
);
119
120
if (!agent) {
···
130
131
router.get("/u/:authorDid", async (req, res) => {
132
const { authorDid } = req.params;
133
-
const pastes = await ctx.db
134
-
.selectFrom("paste")
135
-
.selectAll()
136
-
.where("authorDid", "=", authorDid)
137
-
.orderBy("indexedAt", "desc")
138
-
.execute();
0
0
0
0
0
0
0
0
0
0
139
let didHandleMap = {};
140
didHandleMap[authorDid] = await ctx.resolver.resolveDidToHandle(authorDid);
141
return res.render("user", { pastes, authorDid, didHandleMap });
···
151
if (!ret) {
152
return res.status(404);
153
}
0
0
0
0
0
154
const { authorDid: did, uri } = ret;
155
-
const handle = await ctx.resolver.resolveDidToHandle(did);
0
0
156
const resolver = new DidResolver({});
157
const didDocument = await resolver.resolve(did);
158
if (!didDocument) {
···
162
if (!pds) {
163
return res.status(404);
164
}
0
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);
170
-
171
-
const response = await fetch(url.toString());
172
-
173
-
if (!response.ok) {
174
-
return res.status(404);
175
-
}
176
177
-
const pasteRecord = await response.json();
178
const paste =
179
-
Paste.isRecord(pasteRecord.value) &&
180
-
Paste.validateRecord(pasteRecord.value).success
181
-
? pasteRecord.value
182
: {};
183
184
-
return res.render("paste", { paste, handle, shortUrl });
185
});
186
0
0
0
187
router.get("/r/:shortUrl", async (req, res) => {
188
const { shortUrl } = req.params;
189
const ret = await ctx.db
···
199
return res.send(ret.code);
200
});
201
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
202
router.post("/paste", async (req, res) => {
203
const agent = await getSessionAgent(req, res, ctx);
204
if (!agent) {
···
209
}
210
211
const rkey = TID.nextStr();
0
212
const record = {
213
$type: "ovh.plonk.paste",
214
code: req.body?.code,
215
lang: req.body?.lang,
0
216
title: req.body?.title,
217
createdAt: new Date().toISOString(),
218
};
···
259
.execute();
260
ctx.logger.info(res, "wrote back to db");
261
return res.redirect(`/p/${shortUrl}`);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
262
} catch (err) {
263
ctx.logger.warn(
264
{ err },
···
8
import { Agent } from "@atproto/api";
9
import { getPds, DidResolver } from "@atproto/identity";
10
import { TID } from "@atproto/common";
11
+
import { Agent } from "@atproto/api";
12
import { newShortUrl } from "#/db";
13
14
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
15
+
import * as Comment from "#/lexicons/types/ovh/plonk/comment";
16
+
import { ComAtprotoRepoNS } from "#/lexicons";
17
18
type Session = {
19
did: string;
···
117
118
// Map user DIDs to their domain-name handles
119
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
120
+
pastes.map((s) => s.authorDid).concat(agent? [agent.assertDid]:[]),
121
);
122
123
if (!agent) {
···
133
134
router.get("/u/:authorDid", async (req, res) => {
135
const { authorDid } = req.params;
136
+
const resolver = new DidResolver({});
137
+
const didDocument = await resolver.resolve(authorDid);
138
+
if (!didDocument) {
139
+
return res.status(404);
140
+
}
141
+
const pds = getPds(didDocument);
142
+
if (!pds) {
143
+
return res.status(404);
144
+
}
145
+
const agent = new Agent(pds);
146
+
const response = await agent.com.atproto.repo.listRecords({
147
+
repo: authorDid,
148
+
collection: 'ovh.plonk.paste',
149
+
limit: 99,
150
+
});
151
+
const pastes = response.data.records;
152
let didHandleMap = {};
153
didHandleMap[authorDid] = await ctx.resolver.resolveDidToHandle(authorDid);
154
return res.render("user", { pastes, authorDid, didHandleMap });
···
164
if (!ret) {
165
return res.status(404);
166
}
167
+
var comments = await ctx.db
168
+
.selectFrom("comment")
169
+
.selectAll()
170
+
.where("pasteUri", '=', ret.uri)
171
+
.execute();
172
const { authorDid: did, uri } = ret;
173
+
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
174
+
comments.map((c) => c.authorDid).concat([did]),
175
+
)
176
const resolver = new DidResolver({});
177
const didDocument = await resolver.resolve(did);
178
if (!didDocument) {
···
182
if (!pds) {
183
return res.status(404);
184
}
185
+
const agent = new Agent(pds);
186
const aturi = new AtUri(uri);
187
+
const response = await agent.com.atproto.repo.getRecord({
188
+
repo: aturi.hostname,
189
+
collection: aturi.collection,
190
+
rkey: aturi.rkey
191
+
});
0
0
0
0
0
192
0
193
const paste =
194
+
Paste.isRecord(response.data.value) &&
195
+
Paste.validateRecord(response.data.value).success
196
+
? response.data.value
197
: {};
198
199
+
return res.render("paste", { paste, authorDid: did, uri: response.data.uri, didHandleMap, shortUrl, comments });
200
});
201
202
+
router.get("/p/:shortUrl/raw", async (req, res) => {
203
+
res.redirect(`/r/${req.params.shortUrl}`)
204
+
});
205
router.get("/r/:shortUrl", async (req, res) => {
206
const { shortUrl } = req.params;
207
const ret = await ctx.db
···
217
return res.send(ret.code);
218
});
219
220
+
router.get("/reset", async (req, res) => {
221
+
const agent = await getSessionAgent(req, res, ctx);
222
+
if (!agent) {
223
+
return res.redirect('/');
224
+
}
225
+
const response = await agent.com.atproto.repo.listRecords({
226
+
repo: agent.assertDid,
227
+
collection: 'ovh.plonk.paste',
228
+
limit: 10,
229
+
});
230
+
const vals = response.data.records;
231
+
for (const v of vals) {
232
+
const aturl = new AtUri(v.uri);
233
+
await agent.com.atproto.repo.deleteRecord({
234
+
repo: agent.assertDid,
235
+
collection: aturl.collection,
236
+
rkey: aturl.rkey,
237
+
});
238
+
}
239
+
return res.redirect('/');
240
+
});
241
+
242
router.post("/paste", async (req, res) => {
243
const agent = await getSessionAgent(req, res, ctx);
244
if (!agent) {
···
249
}
250
251
const rkey = TID.nextStr();
252
+
const shortUrl = await newShortUrl(ctx.db);
253
const record = {
254
$type: "ovh.plonk.paste",
255
code: req.body?.code,
256
lang: req.body?.lang,
257
+
shortUrl,
258
title: req.body?.title,
259
createdAt: new Date().toISOString(),
260
};
···
301
.execute();
302
ctx.logger.info(res, "wrote back to db");
303
return res.redirect(`/p/${shortUrl}`);
304
+
} catch (err) {
305
+
ctx.logger.warn(
306
+
{ err },
307
+
"failed to update computed view; ignoring as it should be caught by the firehose",
308
+
);
309
+
}
310
+
311
+
return res.redirect("/");
312
+
});
313
+
314
+
router.post("/:paste/comment", async (req, res) => {
315
+
const agent = await getSessionAgent(req, res, ctx);
316
+
317
+
if (!agent) {
318
+
return res
319
+
.status(401)
320
+
.type("html")
321
+
.send("<h1>Error: Session required</h1>");
322
+
}
323
+
324
+
const pasteUri = req.params.paste;
325
+
const aturi = new AtUri(pasteUri);
326
+
const pasteResponse = await agent.com.atproto.repo.getRecord({
327
+
repo: aturi.hostname,
328
+
collection: aturi.collection,
329
+
rkey: aturi.rkey
330
+
});
331
+
const pasteCid = pasteResponse.data.cid;
332
+
if (!pasteCid) {
333
+
return res
334
+
.status(401)
335
+
.type("html")
336
+
.send("invalid paste");
337
+
}
338
+
339
+
const rkey = TID.nextStr();
340
+
const record = {
341
+
$type: "ovh.plonk.comment",
342
+
content: req.body?.comment,
343
+
post: {
344
+
uri: pasteUri,
345
+
cid: pasteCid
346
+
},
347
+
createdAt: new Date().toISOString(),
348
+
};
349
+
350
+
if (!Comment.validateRecord(record).success) {
351
+
return res
352
+
.status(400)
353
+
.type("html")
354
+
.send("<h1>Error: Invalid status</h1>");
355
+
}
356
+
357
+
let uri;
358
+
try {
359
+
const res = await agent.com.atproto.repo.putRecord({
360
+
repo: agent.assertDid,
361
+
collection: "ovh.plonk.comment",
362
+
rkey,
363
+
record,
364
+
validate: false,
365
+
});
366
+
uri = res.data.uri;
367
+
} catch (err) {
368
+
ctx.logger.warn({ err }, "failed to put record");
369
+
return res
370
+
.status(500)
371
+
.type("html")
372
+
.send("<h3>Error: Failed to write record</h1>");
373
+
}
374
+
375
+
try {
376
+
await ctx.db
377
+
.insertInto("comment")
378
+
.values({
379
+
uri,
380
+
body: record.content,
381
+
authorDid: agent.assertDid,
382
+
pasteUri: record.post.uri,
383
+
pasteCid: record.post.cid,
384
+
createdAt: record.createdAt,
385
+
indexedAt: new Date().toISOString(),
386
+
})
387
+
.execute();
388
+
ctx.logger.info(res, "wrote back to db");
389
+
const originalPaste = await ctx.db.selectFrom('paste').selectAll().where('uri', '=', pasteUri).executeTakeFirst();
390
+
return res.redirect(`/p/${originalPaste.shortUrl}#${encodeURIComponent(uri)}`);
391
} catch (err) {
392
ctx.logger.warn(
393
{ err },
+2
-1
src/views/index.pug
···
44
45
div.timeline
46
each paste in pastes
47
-
+post(paste, didHandleMap)
0
···
44
45
div.timeline
46
each paste in pastes
47
+
- var handle = didHandleMap[paste.authorDid]
48
+
+post(paste, handle, paste.authorDid)
+19
-1
src/views/paste.pug
···
8
main#content
9
h1 #{paste.title}
10
p
11
-
| by @#{handle} ·
12
| #{timeDifference(now, Date.parse(paste.createdAt))} ago ·
13
| #{paste.lang} ·
14
| #{paste.code.split('\n').length} loc ·
15
a(href=`/r/${shortUrl}`) raw
16
pre
17
| #{paste.code}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
8
main#content
9
h1 #{paste.title}
10
p
11
+
| by @#{didHandleMap[authorDid]} ·
12
| #{timeDifference(now, Date.parse(paste.createdAt))} ago ·
13
| #{paste.lang} ·
14
| #{paste.code.split('\n').length} loc ·
15
a(href=`/r/${shortUrl}`) raw
16
pre
17
| #{paste.code}
18
+
hr
19
+
20
+
div.comments
21
+
each comment in comments
22
+
div.comment(id=`${encodeURIComponent(comment.uri)}`)
23
+
p
24
+
| by @#{didHandleMap[comment.authorDid]} ·
25
+
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
26
+
p
27
+
| #{comment.body}
28
+
hr
29
+
30
+
form(action=`/${encodeURIComponent(uri)}/comment` method="post").post-form
31
+
div.post-row
32
+
textarea#code(name="comment" rows="5" placeholder="add a comment" required).post-input-code
33
+
34
+
div.post-submit-row
35
+
button(type="submit").post-input-submit zonk!
+1
-1
src/views/user.pug
···
11
h1 plonks by @#{handle}
12
div.timeline
13
each paste in pastes
14
-
+post(paste, didHandleMap)
···
11
h1 plonks by @#{handle}
12
div.timeline
13
each paste in pastes
14
+
+post(paste.value, handle, authorDid)
+12
tsup.config.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { defineConfig } from 'tsup';
2
+
3
+
export default defineConfig({
4
+
entry: ['src/index.ts'],
5
+
outDir: 'dist',
6
+
clean: true,
7
+
format: 'esm',
8
+
target: 'node18',
9
+
dts: true,
10
+
minify: true,
11
+
sourcemap: true,
12
+
});