+23
-1
flake.nix
+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
-
import nixpkgs { inherit system; });
13
+
import nixpkgs {
14
+
inherit system;
15
+
overlays = [self.overlay.default];
16
+
});
14
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);
15
37
16
38
devShell = forAllSystems (system: let
17
39
pkgs = nixpkgsFor."${system}";
+17
-2
lexicons/paste.json
+17
-2
lexicons/paste.json
···
7
7
"key": "tid",
8
8
"record": {
9
9
"type": "object",
10
-
"required": ["code", "lang", "title", "createdAt"],
10
+
"required": [
11
+
"code",
12
+
"shortUrl",
13
+
"lang",
14
+
"title",
15
+
"createdAt"
16
+
],
11
17
"properties": {
12
18
"code": {
13
19
"type": "string",
···
15
21
"maxGraphemes": 65536,
16
22
"maxLength": 65536
17
23
},
24
+
"shortUrl": {
25
+
"type": "string",
26
+
"minLength": 2,
27
+
"maxGraphemes": 10,
28
+
"maxLength": 10
29
+
},
18
30
"lang": {
19
31
"type": "string",
20
32
"minLength": 1,
···
27
39
"maxGraphemes": 100,
28
40
"maxLength": 100
29
41
},
30
-
"createdAt": { "type": "string", "format": "datetime" }
42
+
"createdAt": {
43
+
"type": "string",
44
+
"format": "datetime"
45
+
}
31
46
}
32
47
}
33
48
}
+31
src/db.ts
+31
src/db.ts
···
1
1
import SqliteDb from "better-sqlite3";
2
2
import { randomBytes } from "crypto";
3
+
import e from "express";
3
4
4
5
import {
5
6
Kysely,
···
12
13
13
14
export type DatabaseSchema = {
14
15
paste: Paste;
16
+
comment: Comment;
15
17
auth_state: AuthState;
16
18
auth_session: AuthSession;
17
19
};
···
19
21
export type Paste = {
20
22
uri: string;
21
23
authorDid: string;
24
+
shortUrl: string;
22
25
code: string;
23
26
lang: string;
24
27
title: string;
···
36
39
state: AuthStateJson;
37
40
};
38
41
42
+
export type Comment = {
43
+
uri: string;
44
+
authorDid: string;
45
+
body: string;
46
+
createdAt: string;
47
+
indexedAt: string;
48
+
pasteUri: string;
49
+
pasteCid: string;
50
+
}
51
+
39
52
type AuthSessionJson = string;
40
53
type AuthStateJson = string;
41
54
···
85
98
await db.schema.dropTable("paste").execute();
86
99
},
87
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
+
}
88
119
89
120
function generateShortString(length: number): string {
90
121
return randomBytes(length).toString("base64url").substring(0, length);
+37
-5
src/ingester.ts
+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
-
import { newShortUrl } from "#/db";
6
5
import * as Paste from "#/lexicons/types/ovh/plonk/paste";
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
-
// Store the status in our SQLite
25
-
const short_url = await newShortUrl(db);
26
24
await db
27
25
.insertInto("paste")
28
26
.values({
29
27
uri: evt.uri.toString(),
30
-
shortUrl,
28
+
shortUrl: record.shortUrl,
31
29
authorDid: evt.did,
32
30
code: record.code,
33
31
lang: record.lang,
···
44
42
}),
45
43
)
46
44
.execute();
45
+
} else if (
46
+
evt.collection === "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();
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
-
}
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
+
}
58
90
},
59
91
onError: (err) => {
60
92
logger.error({ err }, "error on firehose ingestion");
+40
-16
src/lexicons/index.ts
+40
-16
src/lexicons/index.ts
···
9
9
StreamAuthVerifier,
10
10
} from '@atproto/xrpc-server'
11
11
import { schemas } from './lexicons'
12
+
import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord'
13
+
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords'
12
14
13
15
export function createServer(options?: XrpcOptions): Server {
14
16
return new Server(options)
···
17
19
export class Server {
18
20
xrpc: XrpcServer
19
21
ovh: OvhNS
20
-
app: AppNS
21
22
com: ComNS
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
-
this.app = new AppNS(this)
27
28
this.com = new ComNS(this)
29
+
this.app = new AppNS(this)
28
30
}
29
31
}
30
32
···
46
48
}
47
49
}
48
50
49
-
export class AppNS {
51
+
export class ComNS {
50
52
_server: Server
51
-
bsky: AppBskyNS
53
+
atproto: ComAtprotoNS
52
54
53
55
constructor(server: Server) {
54
56
this._server = server
55
-
this.bsky = new AppBskyNS(server)
57
+
this.atproto = new ComAtprotoNS(server)
56
58
}
57
59
}
58
60
59
-
export class AppBskyNS {
61
+
export class ComAtprotoNS {
60
62
_server: Server
61
-
actor: AppBskyActorNS
63
+
repo: ComAtprotoRepoNS
62
64
63
65
constructor(server: Server) {
64
66
this._server = server
65
-
this.actor = new AppBskyActorNS(server)
67
+
this.repo = new ComAtprotoRepoNS(server)
66
68
}
67
69
}
68
70
69
-
export class AppBskyActorNS {
71
+
export class ComAtprotoRepoNS {
70
72
_server: Server
71
73
72
74
constructor(server: Server) {
73
75
this._server = server
74
76
}
77
+
78
+
getRecord<AV extends AuthVerifier>(
79
+
cfg: ConfigOf<
80
+
AV,
81
+
ComAtprotoRepoGetRecord.Handler<ExtractAuth<AV>>,
82
+
ComAtprotoRepoGetRecord.HandlerReqCtx<ExtractAuth<AV>>
83
+
>,
84
+
) {
85
+
const nsid = 'com.atproto.repo.getRecord' // @ts-ignore
86
+
return this._server.xrpc.method(nsid, cfg)
87
+
}
88
+
89
+
listRecords<AV extends AuthVerifier>(
90
+
cfg: ConfigOf<
91
+
AV,
92
+
ComAtprotoRepoListRecords.Handler<ExtractAuth<AV>>,
93
+
ComAtprotoRepoListRecords.HandlerReqCtx<ExtractAuth<AV>>
94
+
>,
95
+
) {
96
+
const nsid = 'com.atproto.repo.listRecords' // @ts-ignore
97
+
return this._server.xrpc.method(nsid, cfg)
98
+
}
75
99
}
76
100
77
-
export class ComNS {
101
+
export class AppNS {
78
102
_server: Server
79
-
atproto: ComAtprotoNS
103
+
bsky: AppBskyNS
80
104
81
105
constructor(server: Server) {
82
106
this._server = server
83
-
this.atproto = new ComAtprotoNS(server)
107
+
this.bsky = new AppBskyNS(server)
84
108
}
85
109
}
86
110
87
-
export class ComAtprotoNS {
111
+
export class AppBskyNS {
88
112
_server: Server
89
-
repo: ComAtprotoRepoNS
113
+
actor: AppBskyActorNS
90
114
91
115
constructor(server: Server) {
92
116
this._server = server
93
-
this.repo = new ComAtprotoRepoNS(server)
117
+
this.actor = new AppBskyActorNS(server)
94
118
}
95
119
}
96
120
97
-
export class ComAtprotoRepoNS {
121
+
export class AppBskyActorNS {
98
122
_server: Server
99
123
100
124
constructor(server: Server) {
+190
-1
src/lexicons/lexicons.ts
+190
-1
src/lexicons/lexicons.ts
···
4
4
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
5
5
6
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
+
},
7
38
ComAtprotoLabelDefs: {
8
39
lexicon: 1,
9
40
id: 'com.atproto.label.defs',
···
183
214
},
184
215
},
185
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
+
},
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
-
required: ['code', 'lang', 'title', 'createdAt'],
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
+
},
383
+
shortUrl: {
384
+
type: 'string',
385
+
minLength: 2,
386
+
maxGraphemes: 10,
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
+
OvhPlonkComment: 'ovh.plonk.comment',
304
491
ComAtprotoLabelDefs: 'com.atproto.label.defs',
492
+
ComAtprotoRepoGetRecord: 'com.atproto.repo.getRecord',
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
+55
src/lexicons/types/com/atproto/repo/getRecord.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import express from 'express'
5
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+
import { lexicons } from '../../../../lexicons'
7
+
import { isObj, hasProp } from '../../../../util'
8
+
import { CID } from 'multiformats/cid'
9
+
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+
11
+
export interface QueryParams {
12
+
/** The handle or DID of the repo. */
13
+
repo: string
14
+
/** The NSID of the record collection. */
15
+
collection: string
16
+
/** The Record Key. */
17
+
rkey: string
18
+
/** The CID of the version of the record. If not specified, then return the most recent version. */
19
+
cid?: string
20
+
}
21
+
22
+
export type InputSchema = undefined
23
+
24
+
export interface OutputSchema {
25
+
uri: string
26
+
cid?: string
27
+
value: {}
28
+
[k: string]: unknown
29
+
}
30
+
31
+
export type HandlerInput = undefined
32
+
33
+
export interface HandlerSuccess {
34
+
encoding: 'application/json'
35
+
body: OutputSchema
36
+
headers?: { [key: string]: string }
37
+
}
38
+
39
+
export interface HandlerError {
40
+
status: number
41
+
message?: string
42
+
error?: 'RecordNotFound'
43
+
}
44
+
45
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
46
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
47
+
auth: HA
48
+
params: QueryParams
49
+
input: HandlerInput
50
+
req: express.Request
51
+
res: express.Response
52
+
}
53
+
export type Handler<HA extends HandlerAuth = never> = (
54
+
ctx: HandlerReqCtx<HA>,
55
+
) => Promise<HandlerOutput> | HandlerOutput
+77
src/lexicons/types/com/atproto/repo/listRecords.ts
+77
src/lexicons/types/com/atproto/repo/listRecords.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import express from 'express'
5
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+
import { lexicons } from '../../../../lexicons'
7
+
import { isObj, hasProp } from '../../../../util'
8
+
import { CID } from 'multiformats/cid'
9
+
import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+
11
+
export interface QueryParams {
12
+
/** The handle or DID of the repo. */
13
+
repo: string
14
+
/** The NSID of the record type. */
15
+
collection: string
16
+
/** The number of records to return. */
17
+
limit: number
18
+
cursor?: string
19
+
/** DEPRECATED: The lowest sort-ordered rkey to start from (exclusive) */
20
+
rkeyStart?: string
21
+
/** DEPRECATED: The highest sort-ordered rkey to stop at (exclusive) */
22
+
rkeyEnd?: string
23
+
/** Flag to reverse the order of the returned records. */
24
+
reverse?: boolean
25
+
}
26
+
27
+
export type InputSchema = undefined
28
+
29
+
export interface OutputSchema {
30
+
cursor?: string
31
+
records: Record[]
32
+
[k: string]: unknown
33
+
}
34
+
35
+
export type HandlerInput = undefined
36
+
37
+
export interface HandlerSuccess {
38
+
encoding: 'application/json'
39
+
body: OutputSchema
40
+
headers?: { [key: string]: string }
41
+
}
42
+
43
+
export interface HandlerError {
44
+
status: number
45
+
message?: string
46
+
}
47
+
48
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
49
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
50
+
auth: HA
51
+
params: QueryParams
52
+
input: HandlerInput
53
+
req: express.Request
54
+
res: express.Response
55
+
}
56
+
export type Handler<HA extends HandlerAuth = never> = (
57
+
ctx: HandlerReqCtx<HA>,
58
+
) => Promise<HandlerOutput> | HandlerOutput
59
+
60
+
export interface Record {
61
+
uri: string
62
+
cid: string
63
+
value: {}
64
+
[k: string]: unknown
65
+
}
66
+
67
+
export function isRecord(v: unknown): v is Record {
68
+
return (
69
+
isObj(v) &&
70
+
hasProp(v, '$type') &&
71
+
v.$type === 'com.atproto.repo.listRecords#record'
72
+
)
73
+
}
74
+
75
+
export function validateRecord(v: unknown): ValidationResult {
76
+
return lexicons.validate('com.atproto.repo.listRecords#record', v)
77
+
}
+29
src/lexicons/types/ovh/plonk/comment.ts
+29
src/lexicons/types/ovh/plonk/comment.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { ValidationResult, BlobRef } from '@atproto/lexicon'
5
+
import { lexicons } from '../../../lexicons'
6
+
import { isObj, hasProp } from '../../../util'
7
+
import { CID } from 'multiformats/cid'
8
+
import * as ComAtprotoRepoStrongRef from '../../com/atproto/repo/strongRef'
9
+
10
+
export interface Record {
11
+
/** comment body */
12
+
content: string
13
+
/** comment creation timestamp */
14
+
createdAt: string
15
+
post: ComAtprotoRepoStrongRef.Main
16
+
[k: string]: unknown
17
+
}
18
+
19
+
export function isRecord(v: unknown): v is Record {
20
+
return (
21
+
isObj(v) &&
22
+
hasProp(v, '$type') &&
23
+
(v.$type === '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
+1
src/lexicons/types/ovh/plonk/paste.ts
+3
-3
src/mixins/post.pug
+3
-3
src/mixins/post.pug
···
1
-
mixin post(paste, didHandleMap)
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
-
a(href=`/u/${encodeURIComponent(paste.authorDid)}`)
9
-
| @#{didHandleMap[paste.authorDid]}
8
+
a(href=`/u/${did}`)
9
+
| @#{handle}
10
10
| ·
11
11
| #{timeDifference(now, Date.parse(paste.createdAt))} ago
12
12
| ·
+1
-1
src/public/styles.css
+1
-1
src/public/styles.css
+152
-23
src/routes.ts
+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
+
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
+
import * as Comment from "#/lexicons/types/ovh/plonk/comment";
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
-
pastes.map((s) => s.authorDid),
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
-
const pastes = await ctx.db
134
-
.selectFrom("paste")
135
-
.selectAll()
136
-
.where("authorDid", "=", authorDid)
137
-
.orderBy("indexedAt", "desc")
138
-
.execute();
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;
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
+
var comments = await ctx.db
168
+
.selectFrom("comment")
169
+
.selectAll()
170
+
.where("pasteUri", '=', ret.uri)
171
+
.execute();
154
172
const { authorDid: did, uri } = ret;
155
-
const handle = await ctx.resolver.resolveDidToHandle(did);
173
+
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
174
+
comments.map((c) => c.authorDid).concat([did]),
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
+
const agent = new Agent(pds);
165
186
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
-
}
187
+
const response = await agent.com.atproto.repo.getRecord({
188
+
repo: aturi.hostname,
189
+
collection: aturi.collection,
190
+
rkey: aturi.rkey
191
+
});
176
192
177
-
const pasteRecord = await response.json();
178
193
const paste =
179
-
Paste.isRecord(pasteRecord.value) &&
180
-
Paste.validateRecord(pasteRecord.value).success
181
-
? pasteRecord.value
194
+
Paste.isRecord(response.data.value) &&
195
+
Paste.validateRecord(response.data.value).success
196
+
? response.data.value
182
197
: {};
183
198
184
-
return res.render("paste", { paste, handle, shortUrl });
199
+
return res.render("paste", { paste, authorDid: did, uri: response.data.uri, didHandleMap, shortUrl, comments });
185
200
});
186
201
202
+
router.get("/p/:shortUrl/raw", async (req, res) => {
203
+
res.redirect(`/r/${req.params.shortUrl}`)
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
+
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
+
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
+
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
+
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
+
} 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)}`);
262
391
} catch (err) {
263
392
ctx.logger.warn(
264
393
{ err },
+2
-1
src/views/index.pug
+2
-1
src/views/index.pug
+19
-1
src/views/paste.pug
+19
-1
src/views/paste.pug
···
8
8
main#content
9
9
h1 #{paste.title}
10
10
p
11
-
| by @#{handle} ·
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
+
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
+1
-1
src/views/user.pug