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
10
supportedSystems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"];
11
11
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
12
12
nixpkgsFor = forAllSystems (system:
13
13
-
import nixpkgs { inherit system; });
13
13
+
import nixpkgs {
14
14
+
inherit system;
15
15
+
overlays = [self.overlay.default];
16
16
+
});
14
17
in {
18
18
+
overlay.default = final: prev: let
19
19
+
pname = "plonk";
20
20
+
version = "0.1.0";
21
21
+
in {
22
22
+
plonk = with final;
23
23
+
buildNpmPackage {
24
24
+
inherit pname version;
25
25
+
src = ./.;
26
26
+
packageJson = ./package.json;
27
27
+
buildPhase = "npm run build";
28
28
+
npmDepsHash = "sha256-qGCbaFAHd/s9hOTWMjHCam6Kf6pU6IWPybfwYh0sOwc=";
29
29
+
};
30
30
+
};
31
31
+
32
32
+
packages = forAllSystems (system: {
33
33
+
inherit (nixpkgsFor."${system}") plonk;
34
34
+
});
35
35
+
36
36
+
defaultPackage = forAllSystems (system: nixpkgsFor."${system}".plonk);
15
37
16
38
devShell = forAllSystems (system: let
17
39
pkgs = nixpkgsFor."${system}";
+17
-2
lexicons/paste.json
···
7
7
"key": "tid",
8
8
"record": {
9
9
"type": "object",
10
10
-
"required": ["code", "lang", "title", "createdAt"],
10
10
+
"required": [
11
11
+
"code",
12
12
+
"shortUrl",
13
13
+
"lang",
14
14
+
"title",
15
15
+
"createdAt"
16
16
+
],
11
17
"properties": {
12
18
"code": {
13
19
"type": "string",
···
15
21
"maxGraphemes": 65536,
16
22
"maxLength": 65536
17
23
},
24
24
+
"shortUrl": {
25
25
+
"type": "string",
26
26
+
"minLength": 2,
27
27
+
"maxGraphemes": 10,
28
28
+
"maxLength": 10
29
29
+
},
18
30
"lang": {
19
31
"type": "string",
20
32
"minLength": 1,
···
27
39
"maxGraphemes": 100,
28
40
"maxLength": 100
29
41
},
30
30
-
"createdAt": { "type": "string", "format": "datetime" }
42
42
+
"createdAt": {
43
43
+
"type": "string",
44
44
+
"format": "datetime"
45
45
+
}
31
46
}
32
47
}
33
48
}
+31
src/db.ts
···
1
1
import SqliteDb from "better-sqlite3";
2
2
import { randomBytes } from "crypto";
3
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
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
24
+
shortUrl: string;
22
25
code: string;
23
26
lang: string;
24
27
title: string;
···
36
39
state: AuthStateJson;
37
40
};
38
41
42
42
+
export type Comment = {
43
43
+
uri: string;
44
44
+
authorDid: string;
45
45
+
body: string;
46
46
+
createdAt: string;
47
47
+
indexedAt: string;
48
48
+
pasteUri: string;
49
49
+
pasteCid: string;
50
50
+
}
51
51
+
39
52
type AuthSessionJson = string;
40
53
type AuthStateJson = string;
41
54
···
85
98
await db.schema.dropTable("paste").execute();
86
99
},
87
100
};
101
101
+
102
102
+
migrations["002"] = {
103
103
+
async up(db: Kysely<unknown>) {
104
104
+
await db.schema
105
105
+
.createTable("comment")
106
106
+
.addColumn("uri", "varchar", (col) => col.primaryKey())
107
107
+
.addColumn("authorDid", "varchar", (col) => col.notNull())
108
108
+
.addColumn("body", "varchar", (col) => col.notNull())
109
109
+
.addColumn("createdAt", "varchar", (col) => col.notNull())
110
110
+
.addColumn("indexedAt", "varchar", (col) => col.notNull())
111
111
+
.addColumn("pasteUri", "varchar", (col) => col.notNull())
112
112
+
.addColumn("pasteCid", "varchar", (col) => col.notNull())
113
113
+
.execute();
114
114
+
},
115
115
+
async down(db: Kysely<unknown>) {
116
116
+
await db.schema.dropTable("comments").execute();
117
117
+
},
118
118
+
}
88
119
89
120
function generateShortString(length: number): string {
90
121
return randomBytes(length).toString("base64url").substring(0, length);
+37
-5
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
5
-
import { newShortUrl } from "#/db";
6
5
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
6
6
+
import * as Comment from "#/lexicons/types/ovh/plonk/comment";
7
7
8
8
export function createIngester(db: Database, idResolver: IdResolver) {
9
9
const logger = pino({ name: "firehose ingestion" });
···
21
21
Paste.isRecord(record) &&
22
22
Paste.validateRecord(record).success
23
23
) {
24
24
-
// Store the status in our SQLite
25
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
30
-
shortUrl,
28
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
45
+
} else if (
46
46
+
evt.collection === "ovh.plonk.comment" &&
47
47
+
Comment.isRecord(record) &&
48
48
+
Comment.validateRecord(record).success
49
49
+
) {
50
50
+
await db
51
51
+
.insertInto("comment")
52
52
+
.values({
53
53
+
uri: evt.uri.toString(),
54
54
+
authorDid: evt.did,
55
55
+
body: record.content,
56
56
+
pasteUri: record.post.uri,
57
57
+
pasteCid: record.post.cid,
58
58
+
createdAt: record.createdAt,
59
59
+
indexedAt: now.toISOString(),
60
60
+
})
61
61
+
.onConflict((oc) =>
62
62
+
oc.column("uri").doUpdateSet({
63
63
+
body: record.content,
64
64
+
pasteUri: record.post.uri,
65
65
+
pasteCid: record.post.cid,
66
66
+
indexedAt: now.toISOString(),
67
67
+
}),
68
68
+
)
69
69
+
.execute();
47
70
}
48
71
} else if (
49
72
evt.event === "delete" &&
···
54
77
.deleteFrom("paste")
55
78
.where("uri", "=", evt.uri.toString())
56
79
.execute();
57
57
-
}
80
80
+
} else if (
81
81
+
evt.event === "delete" &&
82
82
+
evt.collection === "ovh.plonk.comment"
83
83
+
) {
84
84
+
// Remove the status from our SQLite
85
85
+
await db
86
86
+
.deleteFrom("comment")
87
87
+
.where("uri", "=", evt.uri.toString())
88
88
+
.execute();
89
89
+
}
58
90
},
59
91
onError: (err) => {
60
92
logger.error({ err }, "error on firehose ingestion");
+40
-16
src/lexicons/index.ts
···
9
9
StreamAuthVerifier,
10
10
} from '@atproto/xrpc-server'
11
11
import { schemas } from './lexicons'
12
12
+
import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord'
13
13
+
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords'
12
14
13
15
export function createServer(options?: XrpcOptions): Server {
14
16
return new Server(options)
···
17
19
export class Server {
18
20
xrpc: XrpcServer
19
21
ovh: OvhNS
20
20
-
app: AppNS
21
22
com: ComNS
23
23
+
app: AppNS
22
24
23
25
constructor(options?: XrpcOptions) {
24
26
this.xrpc = createXrpcServer(schemas, options)
25
27
this.ovh = new OvhNS(this)
26
26
-
this.app = new AppNS(this)
27
28
this.com = new ComNS(this)
29
29
+
this.app = new AppNS(this)
28
30
}
29
31
}
30
32
···
46
48
}
47
49
}
48
50
49
49
-
export class AppNS {
51
51
+
export class ComNS {
50
52
_server: Server
51
51
-
bsky: AppBskyNS
53
53
+
atproto: ComAtprotoNS
52
54
53
55
constructor(server: Server) {
54
56
this._server = server
55
55
-
this.bsky = new AppBskyNS(server)
57
57
+
this.atproto = new ComAtprotoNS(server)
56
58
}
57
59
}
58
60
59
59
-
export class AppBskyNS {
61
61
+
export class ComAtprotoNS {
60
62
_server: Server
61
61
-
actor: AppBskyActorNS
63
63
+
repo: ComAtprotoRepoNS
62
64
63
65
constructor(server: Server) {
64
66
this._server = server
65
65
-
this.actor = new AppBskyActorNS(server)
67
67
+
this.repo = new ComAtprotoRepoNS(server)
66
68
}
67
69
}
68
70
69
69
-
export class AppBskyActorNS {
71
71
+
export class ComAtprotoRepoNS {
70
72
_server: Server
71
73
72
74
constructor(server: Server) {
73
75
this._server = server
74
76
}
77
77
+
78
78
+
getRecord<AV extends AuthVerifier>(
79
79
+
cfg: ConfigOf<
80
80
+
AV,
81
81
+
ComAtprotoRepoGetRecord.Handler<ExtractAuth<AV>>,
82
82
+
ComAtprotoRepoGetRecord.HandlerReqCtx<ExtractAuth<AV>>
83
83
+
>,
84
84
+
) {
85
85
+
const nsid = 'com.atproto.repo.getRecord' // @ts-ignore
86
86
+
return this._server.xrpc.method(nsid, cfg)
87
87
+
}
88
88
+
89
89
+
listRecords<AV extends AuthVerifier>(
90
90
+
cfg: ConfigOf<
91
91
+
AV,
92
92
+
ComAtprotoRepoListRecords.Handler<ExtractAuth<AV>>,
93
93
+
ComAtprotoRepoListRecords.HandlerReqCtx<ExtractAuth<AV>>
94
94
+
>,
95
95
+
) {
96
96
+
const nsid = 'com.atproto.repo.listRecords' // @ts-ignore
97
97
+
return this._server.xrpc.method(nsid, cfg)
98
98
+
}
75
99
}
76
100
77
77
-
export class ComNS {
101
101
+
export class AppNS {
78
102
_server: Server
79
79
-
atproto: ComAtprotoNS
103
103
+
bsky: AppBskyNS
80
104
81
105
constructor(server: Server) {
82
106
this._server = server
83
83
-
this.atproto = new ComAtprotoNS(server)
107
107
+
this.bsky = new AppBskyNS(server)
84
108
}
85
109
}
86
110
87
87
-
export class ComAtprotoNS {
111
111
+
export class AppBskyNS {
88
112
_server: Server
89
89
-
repo: ComAtprotoRepoNS
113
113
+
actor: AppBskyActorNS
90
114
91
115
constructor(server: Server) {
92
116
this._server = server
93
93
-
this.repo = new ComAtprotoRepoNS(server)
117
117
+
this.actor = new AppBskyActorNS(server)
94
118
}
95
119
}
96
120
97
97
-
export class ComAtprotoRepoNS {
121
121
+
export class AppBskyActorNS {
98
122
_server: Server
99
123
100
124
constructor(server: Server) {
+190
-1
src/lexicons/lexicons.ts
···
4
4
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
5
5
6
6
export const schemaDict = {
7
7
+
OvhPlonkComment: {
8
8
+
lexicon: 1,
9
9
+
id: 'ovh.plonk.comment',
10
10
+
defs: {
11
11
+
main: {
12
12
+
type: 'record',
13
13
+
key: 'tid',
14
14
+
record: {
15
15
+
type: 'object',
16
16
+
required: ['content', 'createdAt', 'post'],
17
17
+
properties: {
18
18
+
content: {
19
19
+
type: 'string',
20
20
+
maxLength: 100000,
21
21
+
maxGraphemes: 10000,
22
22
+
description: 'comment body',
23
23
+
},
24
24
+
createdAt: {
25
25
+
type: 'string',
26
26
+
format: 'datetime',
27
27
+
description: 'comment creation timestamp',
28
28
+
},
29
29
+
post: {
30
30
+
type: 'ref',
31
31
+
ref: 'lex:com.atproto.repo.strongRef',
32
32
+
},
33
33
+
},
34
34
+
},
35
35
+
},
36
36
+
},
37
37
+
},
7
38
ComAtprotoLabelDefs: {
8
39
lexicon: 1,
9
40
id: 'com.atproto.label.defs',
···
183
214
},
184
215
},
185
216
},
217
217
+
ComAtprotoRepoGetRecord: {
218
218
+
lexicon: 1,
219
219
+
id: 'com.atproto.repo.getRecord',
220
220
+
defs: {
221
221
+
main: {
222
222
+
type: 'query',
223
223
+
description:
224
224
+
'Get a single record from a repository. Does not require auth.',
225
225
+
parameters: {
226
226
+
type: 'params',
227
227
+
required: ['repo', 'collection', 'rkey'],
228
228
+
properties: {
229
229
+
repo: {
230
230
+
type: 'string',
231
231
+
format: 'at-identifier',
232
232
+
description: 'The handle or DID of the repo.',
233
233
+
},
234
234
+
collection: {
235
235
+
type: 'string',
236
236
+
format: 'nsid',
237
237
+
description: 'The NSID of the record collection.',
238
238
+
},
239
239
+
rkey: {
240
240
+
type: 'string',
241
241
+
description: 'The Record Key.',
242
242
+
},
243
243
+
cid: {
244
244
+
type: 'string',
245
245
+
format: 'cid',
246
246
+
description:
247
247
+
'The CID of the version of the record. If not specified, then return the most recent version.',
248
248
+
},
249
249
+
},
250
250
+
},
251
251
+
output: {
252
252
+
encoding: 'application/json',
253
253
+
schema: {
254
254
+
type: 'object',
255
255
+
required: ['uri', 'value'],
256
256
+
properties: {
257
257
+
uri: {
258
258
+
type: 'string',
259
259
+
format: 'at-uri',
260
260
+
},
261
261
+
cid: {
262
262
+
type: 'string',
263
263
+
format: 'cid',
264
264
+
},
265
265
+
value: {
266
266
+
type: 'unknown',
267
267
+
},
268
268
+
},
269
269
+
},
270
270
+
},
271
271
+
errors: [
272
272
+
{
273
273
+
name: 'RecordNotFound',
274
274
+
},
275
275
+
],
276
276
+
},
277
277
+
},
278
278
+
},
279
279
+
ComAtprotoRepoListRecords: {
280
280
+
lexicon: 1,
281
281
+
id: 'com.atproto.repo.listRecords',
282
282
+
defs: {
283
283
+
main: {
284
284
+
type: 'query',
285
285
+
description:
286
286
+
'List a range of records in a repository, matching a specific collection. Does not require auth.',
287
287
+
parameters: {
288
288
+
type: 'params',
289
289
+
required: ['repo', 'collection'],
290
290
+
properties: {
291
291
+
repo: {
292
292
+
type: 'string',
293
293
+
format: 'at-identifier',
294
294
+
description: 'The handle or DID of the repo.',
295
295
+
},
296
296
+
collection: {
297
297
+
type: 'string',
298
298
+
format: 'nsid',
299
299
+
description: 'The NSID of the record type.',
300
300
+
},
301
301
+
limit: {
302
302
+
type: 'integer',
303
303
+
minimum: 1,
304
304
+
maximum: 100,
305
305
+
default: 50,
306
306
+
description: 'The number of records to return.',
307
307
+
},
308
308
+
cursor: {
309
309
+
type: 'string',
310
310
+
},
311
311
+
rkeyStart: {
312
312
+
type: 'string',
313
313
+
description:
314
314
+
'DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)',
315
315
+
},
316
316
+
rkeyEnd: {
317
317
+
type: 'string',
318
318
+
description:
319
319
+
'DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)',
320
320
+
},
321
321
+
reverse: {
322
322
+
type: 'boolean',
323
323
+
description: 'Flag to reverse the order of the returned records.',
324
324
+
},
325
325
+
},
326
326
+
},
327
327
+
output: {
328
328
+
encoding: 'application/json',
329
329
+
schema: {
330
330
+
type: 'object',
331
331
+
required: ['records'],
332
332
+
properties: {
333
333
+
cursor: {
334
334
+
type: 'string',
335
335
+
},
336
336
+
records: {
337
337
+
type: 'array',
338
338
+
items: {
339
339
+
type: 'ref',
340
340
+
ref: 'lex:com.atproto.repo.listRecords#record',
341
341
+
},
342
342
+
},
343
343
+
},
344
344
+
},
345
345
+
},
346
346
+
},
347
347
+
record: {
348
348
+
type: 'object',
349
349
+
required: ['uri', 'cid', 'value'],
350
350
+
properties: {
351
351
+
uri: {
352
352
+
type: 'string',
353
353
+
format: 'at-uri',
354
354
+
},
355
355
+
cid: {
356
356
+
type: 'string',
357
357
+
format: 'cid',
358
358
+
},
359
359
+
value: {
360
360
+
type: 'unknown',
361
361
+
},
362
362
+
},
363
363
+
},
364
364
+
},
365
365
+
},
186
366
OvhPlonkPaste: {
187
367
lexicon: 1,
188
368
id: 'ovh.plonk.paste',
···
192
372
key: 'tid',
193
373
record: {
194
374
type: 'object',
195
195
-
required: ['code', 'lang', 'title', 'createdAt'],
375
375
+
required: ['code', 'shortUrl', 'lang', 'title', 'createdAt'],
196
376
properties: {
197
377
code: {
198
378
type: 'string',
199
379
minLength: 1,
200
380
maxGraphemes: 65536,
201
381
maxLength: 65536,
382
382
+
},
383
383
+
shortUrl: {
384
384
+
type: 'string',
385
385
+
minLength: 2,
386
386
+
maxGraphemes: 10,
387
387
+
maxLength: 10,
202
388
},
203
389
lang: {
204
390
type: 'string',
···
301
487
export const schemas: LexiconDoc[] = Object.values(schemaDict) as LexiconDoc[]
302
488
export const lexicons: Lexicons = new Lexicons(schemas)
303
489
export const ids = {
490
490
+
OvhPlonkComment: 'ovh.plonk.comment',
304
491
ComAtprotoLabelDefs: 'com.atproto.label.defs',
492
492
+
ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',
493
493
+
ComAtprotoRepoListRecords: 'com.atproto.repo.listRecords',
305
494
OvhPlonkPaste: 'ovh.plonk.paste',
306
495
AppBskyActorProfile: 'app.bsky.actor.profile',
307
496
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
+55
src/lexicons/types/com/atproto/repo/getRecord.ts
···
1
1
+
/**
2
2
+
* GENERATED CODE - DO NOT MODIFY
3
3
+
*/
4
4
+
import express from 'express'
5
5
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
6
+
import { lexicons } from '../../../../lexicons'
7
7
+
import { isObj, hasProp } from '../../../../util'
8
8
+
import { CID } from 'multiformats/cid'
9
9
+
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
10
+
11
11
+
export interface QueryParams {
12
12
+
/** The handle or DID of the repo. */
13
13
+
repo: string
14
14
+
/** The NSID of the record collection. */
15
15
+
collection: string
16
16
+
/** The Record Key. */
17
17
+
rkey: string
18
18
+
/** The CID of the version of the record. If not specified, then return the most recent version. */
19
19
+
cid?: string
20
20
+
}
21
21
+
22
22
+
export type InputSchema = undefined
23
23
+
24
24
+
export interface OutputSchema {
25
25
+
uri: string
26
26
+
cid?: string
27
27
+
value: {}
28
28
+
[k: string]: unknown
29
29
+
}
30
30
+
31
31
+
export type HandlerInput = undefined
32
32
+
33
33
+
export interface HandlerSuccess {
34
34
+
encoding: 'application/json'
35
35
+
body: OutputSchema
36
36
+
headers?: { [key: string]: string }
37
37
+
}
38
38
+
39
39
+
export interface HandlerError {
40
40
+
status: number
41
41
+
message?: string
42
42
+
error?: 'RecordNotFound'
43
43
+
}
44
44
+
45
45
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
46
46
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
47
47
+
auth: HA
48
48
+
params: QueryParams
49
49
+
input: HandlerInput
50
50
+
req: express.Request
51
51
+
res: express.Response
52
52
+
}
53
53
+
export type Handler<HA extends HandlerAuth = never> = (
54
54
+
ctx: HandlerReqCtx<HA>,
55
55
+
) => Promise<HandlerOutput> | HandlerOutput
+77
src/lexicons/types/com/atproto/repo/listRecords.ts
···
1
1
+
/**
2
2
+
* GENERATED CODE - DO NOT MODIFY
3
3
+
*/
4
4
+
import express from 'express'
5
5
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
6
+
import { lexicons } from '../../../../lexicons'
7
7
+
import { isObj, hasProp } from '../../../../util'
8
8
+
import { CID } from 'multiformats/cid'
9
9
+
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
10
+
11
11
+
export interface QueryParams {
12
12
+
/** The handle or DID of the repo. */
13
13
+
repo: string
14
14
+
/** The NSID of the record type. */
15
15
+
collection: string
16
16
+
/** The number of records to return. */
17
17
+
limit: number
18
18
+
cursor?: string
19
19
+
/** DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) */
20
20
+
rkeyStart?: string
21
21
+
/** DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) */
22
22
+
rkeyEnd?: string
23
23
+
/** Flag to reverse the order of the returned records. */
24
24
+
reverse?: boolean
25
25
+
}
26
26
+
27
27
+
export type InputSchema = undefined
28
28
+
29
29
+
export interface OutputSchema {
30
30
+
cursor?: string
31
31
+
records: Record[]
32
32
+
[k: string]: unknown
33
33
+
}
34
34
+
35
35
+
export type HandlerInput = undefined
36
36
+
37
37
+
export interface HandlerSuccess {
38
38
+
encoding: 'application/json'
39
39
+
body: OutputSchema
40
40
+
headers?: { [key: string]: string }
41
41
+
}
42
42
+
43
43
+
export interface HandlerError {
44
44
+
status: number
45
45
+
message?: string
46
46
+
}
47
47
+
48
48
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
49
49
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
50
50
+
auth: HA
51
51
+
params: QueryParams
52
52
+
input: HandlerInput
53
53
+
req: express.Request
54
54
+
res: express.Response
55
55
+
}
56
56
+
export type Handler<HA extends HandlerAuth = never> = (
57
57
+
ctx: HandlerReqCtx<HA>,
58
58
+
) => Promise<HandlerOutput> | HandlerOutput
59
59
+
60
60
+
export interface Record {
61
61
+
uri: string
62
62
+
cid: string
63
63
+
value: {}
64
64
+
[k: string]: unknown
65
65
+
}
66
66
+
67
67
+
export function isRecord(v: unknown): v is Record {
68
68
+
return (
69
69
+
isObj(v) &&
70
70
+
hasProp(v, '$type') &&
71
71
+
v.$type === 'com.atproto.repo.listRecords#record'
72
72
+
)
73
73
+
}
74
74
+
75
75
+
export function validateRecord(v: unknown): ValidationResult {
76
76
+
return lexicons.validate('com.atproto.repo.listRecords#record', v)
77
77
+
}
+29
src/lexicons/types/ovh/plonk/comment.ts
···
1
1
+
/**
2
2
+
* GENERATED CODE - DO NOT MODIFY
3
3
+
*/
4
4
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
5
5
+
import { lexicons } from '../../../lexicons'
6
6
+
import { isObj, hasProp } from '../../../util'
7
7
+
import { CID } from 'multiformats/cid'
8
8
+
import * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef'
9
9
+
10
10
+
export interface Record {
11
11
+
/** comment body */
12
12
+
content: string
13
13
+
/** comment creation timestamp */
14
14
+
createdAt: string
15
15
+
post: ComAtprotoRepoStrongRef.Main
16
16
+
[k: string]: unknown
17
17
+
}
18
18
+
19
19
+
export function isRecord(v: unknown): v is Record {
20
20
+
return (
21
21
+
isObj(v) &&
22
22
+
hasProp(v, '$type') &&
23
23
+
(v.$type === 'ovh.plonk.comment#main' || v.$type === 'ovh.plonk.comment')
24
24
+
)
25
25
+
}
26
26
+
27
27
+
export function validateRecord(v: unknown): ValidationResult {
28
28
+
return lexicons.validate('ovh.plonk.comment#main', v)
29
29
+
}
+1
src/lexicons/types/ovh/plonk/paste.ts
···
8
8
9
9
export interface Record {
10
10
code: string
11
11
+
shortUrl: string
11
12
lang: string
12
13
title: string
13
14
createdAt: string
+3
-3
src/mixins/post.pug
···
1
1
-
mixin post(paste, didHandleMap)
1
1
+
mixin post(paste, handle, did)
2
2
div.post
3
3
p
4
4
a(href=`/p/${paste.shortUrl}`)
5
5
| #{paste.title}
6
6
p.post-info
7
7
| by
8
8
-
a(href=`/u/${encodeURIComponent(paste.authorDid)}`)
9
9
-
| @#{didHandleMap[paste.authorDid]}
8
8
+
a(href=`/u/${did}`)
9
9
+
| @#{handle}
10
10
| ·
11
11
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
12
12
| ·
+1
-1
src/public/styles.css
···
127
127
align-self: flex-end;
128
128
}
129
129
130
130
-
.timeline {
130
130
+
.timeline, .comments {
131
131
display: flex;
132
132
flex-direction: column;
133
133
gap: 1rem;
+152
-23
src/routes.ts
···
8
8
import { Agent } from "@atproto/api";
9
9
import { getPds, DidResolver } from "@atproto/identity";
10
10
import { TID } from "@atproto/common";
11
11
+
import { Agent } from "@atproto/api";
11
12
import { newShortUrl } from "#/db";
12
13
13
14
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
15
15
+
import * as Comment from "#/lexicons/types/ovh/plonk/comment";
16
16
+
import { ComAtprotoRepoNS } from "#/lexicons";
14
17
15
18
type Session = {
16
19
did: string;
···
114
117
115
118
// Map user DIDs to their domain-name handles
116
119
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
117
117
-
pastes.map((s) => s.authorDid),
120
120
+
pastes.map((s) => s.authorDid).concat(agent? [agent.assertDid]:[]),
118
121
);
119
122
120
123
if (!agent) {
···
130
133
131
134
router.get("/u/:authorDid", async (req, res) => {
132
135
const { authorDid } = req.params;
133
133
-
const pastes = await ctx.db
134
134
-
.selectFrom("paste")
135
135
-
.selectAll()
136
136
-
.where("authorDid", "=", authorDid)
137
137
-
.orderBy("indexedAt", "desc")
138
138
-
.execute();
136
136
+
const resolver = new DidResolver({});
137
137
+
const didDocument = await resolver.resolve(authorDid);
138
138
+
if (!didDocument) {
139
139
+
return res.status(404);
140
140
+
}
141
141
+
const pds = getPds(didDocument);
142
142
+
if (!pds) {
143
143
+
return res.status(404);
144
144
+
}
145
145
+
const agent = new Agent(pds);
146
146
+
const response = await agent.com.atproto.repo.listRecords({
147
147
+
repo: authorDid,
148
148
+
collection: 'ovh.plonk.paste',
149
149
+
limit: 99,
150
150
+
});
151
151
+
const pastes = response.data.records;
139
152
let didHandleMap = {};
140
153
didHandleMap[authorDid] = await ctx.resolver.resolveDidToHandle(authorDid);
141
154
return res.render("user", { pastes, authorDid, didHandleMap });
···
151
164
if (!ret) {
152
165
return res.status(404);
153
166
}
167
167
+
var comments = await ctx.db
168
168
+
.selectFrom("comment")
169
169
+
.selectAll()
170
170
+
.where("pasteUri", '=', ret.uri)
171
171
+
.execute();
154
172
const { authorDid: did, uri } = ret;
155
155
-
const handle = await ctx.resolver.resolveDidToHandle(did);
173
173
+
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
174
174
+
comments.map((c) => c.authorDid).concat([did]),
175
175
+
)
156
176
const resolver = new DidResolver({});
157
177
const didDocument = await resolver.resolve(did);
158
178
if (!didDocument) {
···
162
182
if (!pds) {
163
183
return res.status(404);
164
184
}
185
185
+
const agent = new Agent(pds);
165
186
const aturi = new AtUri(uri);
166
166
-
const url = new URL(`${pds}/xrpc/com.atproto.repo.getRecord`);
167
167
-
url.searchParams.set("repo", aturi.hostname);
168
168
-
url.searchParams.set("collection", aturi.collection);
169
169
-
url.searchParams.set("rkey", aturi.rkey);
170
170
-
171
171
-
const response = await fetch(url.toString());
172
172
-
173
173
-
if (!response.ok) {
174
174
-
return res.status(404);
175
175
-
}
187
187
+
const response = await agent.com.atproto.repo.getRecord({
188
188
+
repo: aturi.hostname,
189
189
+
collection: aturi.collection,
190
190
+
rkey: aturi.rkey
191
191
+
});
176
192
177
177
-
const pasteRecord = await response.json();
178
193
const paste =
179
179
-
Paste.isRecord(pasteRecord.value) &&
180
180
-
Paste.validateRecord(pasteRecord.value).success
181
181
-
? pasteRecord.value
194
194
+
Paste.isRecord(response.data.value) &&
195
195
+
Paste.validateRecord(response.data.value).success
196
196
+
? response.data.value
182
197
: {};
183
198
184
184
-
return res.render("paste", { paste, handle, shortUrl });
199
199
+
return res.render("paste", { paste, authorDid: did, uri: response.data.uri, didHandleMap, shortUrl, comments });
185
200
});
186
201
202
202
+
router.get("/p/:shortUrl/raw", async (req, res) => {
203
203
+
res.redirect(`/r/${req.params.shortUrl}`)
204
204
+
});
187
205
router.get("/r/:shortUrl", async (req, res) => {
188
206
const { shortUrl } = req.params;
189
207
const ret = await ctx.db
···
199
217
return res.send(ret.code);
200
218
});
201
219
220
220
+
router.get("/reset", async (req, res) => {
221
221
+
const agent = await getSessionAgent(req, res, ctx);
222
222
+
if (!agent) {
223
223
+
return res.redirect('/');
224
224
+
}
225
225
+
const response = await agent.com.atproto.repo.listRecords({
226
226
+
repo: agent.assertDid,
227
227
+
collection: 'ovh.plonk.paste',
228
228
+
limit: 10,
229
229
+
});
230
230
+
const vals = response.data.records;
231
231
+
for (const v of vals) {
232
232
+
const aturl = new AtUri(v.uri);
233
233
+
await agent.com.atproto.repo.deleteRecord({
234
234
+
repo: agent.assertDid,
235
235
+
collection: aturl.collection,
236
236
+
rkey: aturl.rkey,
237
237
+
});
238
238
+
}
239
239
+
return res.redirect('/');
240
240
+
});
241
241
+
202
242
router.post("/paste", async (req, res) => {
203
243
const agent = await getSessionAgent(req, res, ctx);
204
244
if (!agent) {
···
209
249
}
210
250
211
251
const rkey = TID.nextStr();
252
252
+
const shortUrl = await newShortUrl(ctx.db);
212
253
const record = {
213
254
$type: "ovh.plonk.paste",
214
255
code: req.body?.code,
215
256
lang: req.body?.lang,
257
257
+
shortUrl,
216
258
title: req.body?.title,
217
259
createdAt: new Date().toISOString(),
218
260
};
···
259
301
.execute();
260
302
ctx.logger.info(res, "wrote back to db");
261
303
return res.redirect(`/p/${shortUrl}`);
304
304
+
} catch (err) {
305
305
+
ctx.logger.warn(
306
306
+
{ err },
307
307
+
"failed to update computed view; ignoring as it should be caught by the firehose",
308
308
+
);
309
309
+
}
310
310
+
311
311
+
return res.redirect("/");
312
312
+
});
313
313
+
314
314
+
router.post("/:paste/comment", async (req, res) => {
315
315
+
const agent = await getSessionAgent(req, res, ctx);
316
316
+
317
317
+
if (!agent) {
318
318
+
return res
319
319
+
.status(401)
320
320
+
.type("html")
321
321
+
.send("<h1>Error: Session required</h1>");
322
322
+
}
323
323
+
324
324
+
const pasteUri = req.params.paste;
325
325
+
const aturi = new AtUri(pasteUri);
326
326
+
const pasteResponse = await agent.com.atproto.repo.getRecord({
327
327
+
repo: aturi.hostname,
328
328
+
collection: aturi.collection,
329
329
+
rkey: aturi.rkey
330
330
+
});
331
331
+
const pasteCid = pasteResponse.data.cid;
332
332
+
if (!pasteCid) {
333
333
+
return res
334
334
+
.status(401)
335
335
+
.type("html")
336
336
+
.send("invalid paste");
337
337
+
}
338
338
+
339
339
+
const rkey = TID.nextStr();
340
340
+
const record = {
341
341
+
$type: "ovh.plonk.comment",
342
342
+
content: req.body?.comment,
343
343
+
post: {
344
344
+
uri: pasteUri,
345
345
+
cid: pasteCid
346
346
+
},
347
347
+
createdAt: new Date().toISOString(),
348
348
+
};
349
349
+
350
350
+
if (!Comment.validateRecord(record).success) {
351
351
+
return res
352
352
+
.status(400)
353
353
+
.type("html")
354
354
+
.send("<h1>Error: Invalid status</h1>");
355
355
+
}
356
356
+
357
357
+
let uri;
358
358
+
try {
359
359
+
const res = await agent.com.atproto.repo.putRecord({
360
360
+
repo: agent.assertDid,
361
361
+
collection: "ovh.plonk.comment",
362
362
+
rkey,
363
363
+
record,
364
364
+
validate: false,
365
365
+
});
366
366
+
uri = res.data.uri;
367
367
+
} catch (err) {
368
368
+
ctx.logger.warn({ err }, "failed to put record");
369
369
+
return res
370
370
+
.status(500)
371
371
+
.type("html")
372
372
+
.send("<h3>Error: Failed to write record</h1>");
373
373
+
}
374
374
+
375
375
+
try {
376
376
+
await ctx.db
377
377
+
.insertInto("comment")
378
378
+
.values({
379
379
+
uri,
380
380
+
body: record.content,
381
381
+
authorDid: agent.assertDid,
382
382
+
pasteUri: record.post.uri,
383
383
+
pasteCid: record.post.cid,
384
384
+
createdAt: record.createdAt,
385
385
+
indexedAt: new Date().toISOString(),
386
386
+
})
387
387
+
.execute();
388
388
+
ctx.logger.info(res, "wrote back to db");
389
389
+
const originalPaste = await ctx.db.selectFrom('paste').selectAll().where('uri', '=', pasteUri).executeTakeFirst();
390
390
+
return res.redirect(`/p/${originalPaste.shortUrl}#${encodeURIComponent(uri)}`);
262
391
} catch (err) {
263
392
ctx.logger.warn(
264
393
{ err },
+2
-1
src/views/index.pug
···
44
44
45
45
div.timeline
46
46
each paste in pastes
47
47
-
+post(paste, didHandleMap)
47
47
+
- var handle = didHandleMap[paste.authorDid]
48
48
+
+post(paste, handle, paste.authorDid)
+19
-1
src/views/paste.pug
···
8
8
main#content
9
9
h1 #{paste.title}
10
10
p
11
11
-
| by @#{handle} ·
11
11
+
| by @#{didHandleMap[authorDid]} ·
12
12
| #{timeDifference(now, Date.parse(paste.createdAt))} ago ·
13
13
| #{paste.lang} ·
14
14
| #{paste.code.split('\n').length} loc ·
15
15
a(href=`/r/${shortUrl}`) raw
16
16
pre
17
17
| #{paste.code}
18
18
+
hr
19
19
+
20
20
+
div.comments
21
21
+
each comment in comments
22
22
+
div.comment(id=`${encodeURIComponent(comment.uri)}`)
23
23
+
p
24
24
+
| by @#{didHandleMap[comment.authorDid]} ·
25
25
+
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
26
26
+
p
27
27
+
| #{comment.body}
28
28
+
hr
29
29
+
30
30
+
form(action=`/${encodeURIComponent(uri)}/comment` method="post").post-form
31
31
+
div.post-row
32
32
+
textarea#code(name="comment" rows="5" placeholder="add a comment" required).post-input-code
33
33
+
34
34
+
div.post-submit-row
35
35
+
button(type="submit").post-input-submit zonk!
+1
-1
src/views/user.pug
···
11
11
h1 plonks by @#{handle}
12
12
div.timeline
13
13
each paste in pastes
14
14
-
+post(paste, didHandleMap)
14
14
+
+post(paste.value, handle, authorDid)
+12
tsup.config.ts
···
1
1
+
import { defineConfig } from 'tsup';
2
2
+
3
3
+
export default defineConfig({
4
4
+
entry: ['src/index.ts'],
5
5
+
outDir: 'dist',
6
6
+
clean: true,
7
7
+
format: 'esm',
8
8
+
target: 'node18',
9
9
+
dts: true,
10
10
+
minify: true,
11
11
+
sourcemap: true,
12
12
+
});