@atproto/bsync 0.0.32 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/logger.js +2 -2
- package/dist/logger.js.map +1 -1
- package/package.json +17 -13
- package/bin/migration-create.ts +0 -38
- package/buf.gen.yaml +0 -12
- package/jest.config.cjs +0 -21
- package/proto/bsync.proto +0 -134
- package/src/client.ts +0 -25
- package/src/config.ts +0 -90
- package/src/context.ts +0 -48
- package/src/db/index.ts +0 -200
- package/src/db/migrations/20240108T220751294Z-init.ts +0 -26
- package/src/db/migrations/20240717T224303472Z-notif-ops.ts +0 -24
- package/src/db/migrations/20250527T022203400Z-add-operation.ts +0 -20
- package/src/db/migrations/20250603T163446567Z-alter-operation.ts +0 -19
- package/src/db/migrations/index.ts +0 -8
- package/src/db/migrations/provider.ts +0 -8
- package/src/db/schema/index.ts +0 -16
- package/src/db/schema/mute_item.ts +0 -13
- package/src/db/schema/mute_op.ts +0 -18
- package/src/db/schema/notif_item.ts +0 -13
- package/src/db/schema/notif_op.ts +0 -16
- package/src/db/schema/operation.ts +0 -20
- package/src/db/types.ts +0 -19
- package/src/index.ts +0 -132
- package/src/logger.ts +0 -26
- package/src/routes/add-mute-operation.ts +0 -154
- package/src/routes/add-notif-operation.ts +0 -80
- package/src/routes/auth.ts +0 -15
- package/src/routes/delete-operations.ts +0 -45
- package/src/routes/index.ts +0 -28
- package/src/routes/put-operation.ts +0 -115
- package/src/routes/scan-mute-operations.ts +0 -65
- package/src/routes/scan-notif-operations.ts +0 -64
- package/src/routes/scan-operations.ts +0 -67
- package/src/routes/util.ts +0 -67
- package/tests/delete-operations.test.ts +0 -108
- package/tests/mutes.test.ts +0 -352
- package/tests/notifications.test.ts +0 -209
- package/tests/operations.test.ts +0 -327
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Kysely, sql } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.createTable('mute_op')
|
|
6
|
-
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
|
7
|
-
.addColumn('type', 'int2', (col) => col.notNull()) // integer enum: 0->add, 1->remove, 2->clear
|
|
8
|
-
.addColumn('actorDid', 'varchar', (col) => col.notNull())
|
|
9
|
-
.addColumn('subject', 'varchar', (col) => col.notNull())
|
|
10
|
-
.addColumn('createdAt', 'timestamptz', (col) =>
|
|
11
|
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
|
|
12
|
-
)
|
|
13
|
-
.execute()
|
|
14
|
-
await db.schema
|
|
15
|
-
.createTable('mute_item')
|
|
16
|
-
.addColumn('actorDid', 'varchar', (col) => col.notNull())
|
|
17
|
-
.addColumn('subject', 'varchar', (col) => col.notNull())
|
|
18
|
-
.addColumn('fromId', 'bigint', (col) => col.notNull())
|
|
19
|
-
.addPrimaryKeyConstraint('mute_item_pkey', ['actorDid', 'subject'])
|
|
20
|
-
.execute()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
24
|
-
await db.schema.dropTable('mute_item').execute()
|
|
25
|
-
await db.schema.dropTable('mute_op').execute()
|
|
26
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Kysely, sql } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.createTable('notif_op')
|
|
6
|
-
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
|
7
|
-
.addColumn('actorDid', 'varchar', (col) => col.notNull())
|
|
8
|
-
.addColumn('priority', 'boolean')
|
|
9
|
-
.addColumn('createdAt', 'timestamptz', (col) =>
|
|
10
|
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
|
|
11
|
-
)
|
|
12
|
-
.execute()
|
|
13
|
-
await db.schema
|
|
14
|
-
.createTable('notif_item')
|
|
15
|
-
.addColumn('actorDid', 'varchar', (col) => col.primaryKey())
|
|
16
|
-
.addColumn('priority', 'boolean', (col) => col.notNull())
|
|
17
|
-
.addColumn('fromId', 'bigint', (col) => col.notNull())
|
|
18
|
-
.execute()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
22
|
-
await db.schema.dropTable('notif_item').execute()
|
|
23
|
-
await db.schema.dropTable('notif_op').execute()
|
|
24
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Kysely, sql } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.createTable('operation')
|
|
6
|
-
.addColumn('id', 'bigserial', (col) => col.primaryKey())
|
|
7
|
-
.addColumn('collection', 'varchar', (col) => col.notNull())
|
|
8
|
-
.addColumn('actorDid', 'varchar', (col) => col.notNull())
|
|
9
|
-
.addColumn('rkey', 'varchar', (col) => col.notNull())
|
|
10
|
-
.addColumn('method', 'int2', (col) => col.notNull())
|
|
11
|
-
.addColumn('payload', sql`bytea`)
|
|
12
|
-
.addColumn('createdAt', 'timestamptz', (col) =>
|
|
13
|
-
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`),
|
|
14
|
-
)
|
|
15
|
-
.execute()
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
19
|
-
await db.schema.dropTable('operation').execute()
|
|
20
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Kysely } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.alterTable('operation')
|
|
6
|
-
.renameColumn('collection', 'namespace')
|
|
7
|
-
.execute()
|
|
8
|
-
|
|
9
|
-
await db.schema.alterTable('operation').renameColumn('rkey', 'key').execute()
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
13
|
-
await db.schema
|
|
14
|
-
.alterTable('operation')
|
|
15
|
-
.renameColumn('namespace', 'collection')
|
|
16
|
-
.execute()
|
|
17
|
-
|
|
18
|
-
await db.schema.alterTable('operation').renameColumn('key', 'rkey').execute()
|
|
19
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// NOTE this file can be edited by hand, but it is also appended to by the migration:create command.
|
|
2
|
-
// It's important that every migration is exported from here with the proper name. We'd simplify
|
|
3
|
-
// this with kysely's FileMigrationProvider, but it doesn't play nicely with the build process.
|
|
4
|
-
|
|
5
|
-
export * as _20240108T220751294Z from './20240108T220751294Z-init.js'
|
|
6
|
-
export * as _20240717T224303472Z from './20240717T224303472Z-notif-ops.js'
|
|
7
|
-
export * as _20250527T022203400Z from './20250527T022203400Z-add-operation.js'
|
|
8
|
-
export * as _20250603T163446567Z from './20250603T163446567Z-alter-operation.js'
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Migration, MigrationProvider } from 'kysely/migration'
|
|
2
|
-
|
|
3
|
-
export class DbMigrationProvider implements MigrationProvider {
|
|
4
|
-
constructor(private migrations: Record<string, Migration>) {}
|
|
5
|
-
async getMigrations(): Promise<Record<string, Migration>> {
|
|
6
|
-
return this.migrations
|
|
7
|
-
}
|
|
8
|
-
}
|
package/src/db/schema/index.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Kysely } from 'kysely'
|
|
2
|
-
import * as muteItem from './mute_item.js'
|
|
3
|
-
import * as muteOp from './mute_op.js'
|
|
4
|
-
import * as notifItem from './notif_item.js'
|
|
5
|
-
import * as notifOp from './notif_op.js'
|
|
6
|
-
import * as op from './operation.js'
|
|
7
|
-
|
|
8
|
-
export type DatabaseSchemaType = muteItem.PartialDB &
|
|
9
|
-
muteOp.PartialDB &
|
|
10
|
-
notifItem.PartialDB &
|
|
11
|
-
notifOp.PartialDB &
|
|
12
|
-
op.PartialDB
|
|
13
|
-
|
|
14
|
-
export type DatabaseSchema = Kysely<DatabaseSchemaType>
|
|
15
|
-
|
|
16
|
-
export default DatabaseSchema
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Selectable } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export interface MuteItem {
|
|
4
|
-
actorDid: string
|
|
5
|
-
subject: string // did or aturi for list
|
|
6
|
-
fromId: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type MuteItemEntry = Selectable<MuteItem>
|
|
10
|
-
|
|
11
|
-
export const tableName = 'mute_item'
|
|
12
|
-
|
|
13
|
-
export type PartialDB = { [tableName]: MuteItem }
|
package/src/db/schema/mute_op.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
-
import { MuteOperation_Type } from '../../proto/bsync_pb.js'
|
|
3
|
-
|
|
4
|
-
export interface MuteOp {
|
|
5
|
-
id: GeneratedAlways<number>
|
|
6
|
-
type: MuteOperation_Type // integer enum: 0->add, 1->remove, 2->clear
|
|
7
|
-
actorDid: string
|
|
8
|
-
subject: string // did or aturi for list
|
|
9
|
-
createdAt: GeneratedAlways<Date>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type MuteOpEntry = Selectable<MuteOp>
|
|
13
|
-
|
|
14
|
-
export const tableName = 'mute_op'
|
|
15
|
-
|
|
16
|
-
export type PartialDB = { [tableName]: MuteOp }
|
|
17
|
-
|
|
18
|
-
export const createMuteOpChannel = 'mute_op_create' // used with listen/notify
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Selectable } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export interface NotifItem {
|
|
4
|
-
actorDid: string
|
|
5
|
-
priority: boolean
|
|
6
|
-
fromId: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type NotifItemEntry = Selectable<NotifItem>
|
|
10
|
-
|
|
11
|
-
export const tableName = 'notif_item'
|
|
12
|
-
|
|
13
|
-
export type PartialDB = { [tableName]: NotifItem }
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { GeneratedAlways, Selectable } from 'kysely'
|
|
2
|
-
|
|
3
|
-
export interface NotifOp {
|
|
4
|
-
id: GeneratedAlways<number>
|
|
5
|
-
actorDid: string
|
|
6
|
-
priority: boolean | null
|
|
7
|
-
createdAt: GeneratedAlways<Date>
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type NotifOpEntry = Selectable<NotifOp>
|
|
11
|
-
|
|
12
|
-
export const tableName = 'notif_op'
|
|
13
|
-
|
|
14
|
-
export type PartialDB = { [tableName]: NotifOp }
|
|
15
|
-
|
|
16
|
-
export const createNotifOpChannel = 'notif_op_create' // used with listen/notify
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { GeneratedAlways } from 'kysely'
|
|
2
|
-
import { Method } from '../../proto/bsync_pb.js'
|
|
3
|
-
|
|
4
|
-
export type OperationMethod = Method.CREATE | Method.UPDATE | Method.DELETE
|
|
5
|
-
|
|
6
|
-
export interface Operation {
|
|
7
|
-
id: GeneratedAlways<number>
|
|
8
|
-
actorDid: string
|
|
9
|
-
namespace: string
|
|
10
|
-
key: string
|
|
11
|
-
method: OperationMethod
|
|
12
|
-
payload: Uint8Array<ArrayBuffer>
|
|
13
|
-
createdAt: GeneratedAlways<Date>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const tableName = 'operation'
|
|
17
|
-
|
|
18
|
-
export type PartialDB = { [tableName]: Operation }
|
|
19
|
-
|
|
20
|
-
export const createOperationChannel = 'operation_create' // used with listen/notify
|
package/src/db/types.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { DynamicModule, RawBuilder, SelectQueryBuilder } from 'kysely'
|
|
2
|
-
// eslint-disable-next-line import/default
|
|
3
|
-
import pg from 'pg'
|
|
4
|
-
type PgPool = pg.Pool
|
|
5
|
-
|
|
6
|
-
export type DbRef =
|
|
7
|
-
| RawBuilder<unknown>
|
|
8
|
-
| ReturnType<DynamicModule<unknown>['ref']>
|
|
9
|
-
|
|
10
|
-
export type AnyQb = SelectQueryBuilder<any, any, any>
|
|
11
|
-
|
|
12
|
-
export type PgOptions = {
|
|
13
|
-
url: string
|
|
14
|
-
pool?: PgPool
|
|
15
|
-
schema?: string
|
|
16
|
-
poolSize?: number
|
|
17
|
-
poolMaxUses?: number
|
|
18
|
-
poolIdleTimeoutMs?: number
|
|
19
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import events, { setMaxListeners } from 'node:events'
|
|
2
|
-
import http from 'node:http'
|
|
3
|
-
import { connectNodeAdapter } from '@connectrpc/connect-node'
|
|
4
|
-
// eslint-disable-next-line import/default
|
|
5
|
-
import httpTerminator from 'http-terminator'
|
|
6
|
-
import { ServerConfig } from './config.js'
|
|
7
|
-
import { AppContext, AppContextOptions } from './context.js'
|
|
8
|
-
import { createMuteOpChannel } from './db/schema/mute_op.js'
|
|
9
|
-
import { createNotifOpChannel } from './db/schema/notif_op.js'
|
|
10
|
-
import { createOperationChannel } from './db/schema/operation.js'
|
|
11
|
-
import { dbLogger, loggerMiddleware } from './logger.js'
|
|
12
|
-
import routes from './routes/index.js'
|
|
13
|
-
|
|
14
|
-
export * from './config.js'
|
|
15
|
-
export * from './client.js'
|
|
16
|
-
export { Database } from './db/index.js'
|
|
17
|
-
export { AppContext } from './context.js'
|
|
18
|
-
export { httpLogger } from './logger.js'
|
|
19
|
-
|
|
20
|
-
type BsyncServiceState = 'initialized' | 'started' | 'destroyed'
|
|
21
|
-
|
|
22
|
-
export class BsyncService {
|
|
23
|
-
public ctx: AppContext
|
|
24
|
-
public server: http.Server
|
|
25
|
-
private terminator: httpTerminator.HttpTerminator
|
|
26
|
-
private ac: AbortController
|
|
27
|
-
private state: BsyncServiceState = 'initialized'
|
|
28
|
-
|
|
29
|
-
constructor(opts: {
|
|
30
|
-
ctx: AppContext
|
|
31
|
-
server: http.Server
|
|
32
|
-
ac: AbortController
|
|
33
|
-
}) {
|
|
34
|
-
this.ctx = opts.ctx
|
|
35
|
-
this.server = opts.server
|
|
36
|
-
this.ac = opts.ac
|
|
37
|
-
this.terminator = httpTerminator.createHttpTerminator(opts)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static async create(
|
|
41
|
-
cfg: ServerConfig,
|
|
42
|
-
overrides?: Partial<AppContextOptions>,
|
|
43
|
-
): Promise<BsyncService> {
|
|
44
|
-
const ac = new AbortController()
|
|
45
|
-
// Prevents unhelpful warnings.
|
|
46
|
-
setMaxListeners(100, ac.signal)
|
|
47
|
-
const ctx = await AppContext.fromConfig(cfg, ac.signal, overrides)
|
|
48
|
-
const handler = connectNodeAdapter({
|
|
49
|
-
routes: routes(ctx),
|
|
50
|
-
shutdownSignal: ac.signal,
|
|
51
|
-
})
|
|
52
|
-
const server = http.createServer((req, res) => {
|
|
53
|
-
loggerMiddleware(req, res)
|
|
54
|
-
if (isHealth(req.url)) {
|
|
55
|
-
res.statusCode = 200
|
|
56
|
-
res.setHeader('content-type', 'application/json')
|
|
57
|
-
return res.end(JSON.stringify({ version: cfg.service.version }))
|
|
58
|
-
}
|
|
59
|
-
handler(req, res)
|
|
60
|
-
})
|
|
61
|
-
return new BsyncService({ ctx, server, ac })
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async start(): Promise<http.Server> {
|
|
65
|
-
if (this.state !== 'initialized') {
|
|
66
|
-
throw new Error(`${this.constructor.name} already started`)
|
|
67
|
-
}
|
|
68
|
-
this.state = 'started'
|
|
69
|
-
|
|
70
|
-
const dbStatsInterval = setInterval(() => {
|
|
71
|
-
dbLogger.info(
|
|
72
|
-
{
|
|
73
|
-
idleCount: this.ctx.db.pool.idleCount,
|
|
74
|
-
totalCount: this.ctx.db.pool.totalCount,
|
|
75
|
-
waitingCount: this.ctx.db.pool.waitingCount,
|
|
76
|
-
},
|
|
77
|
-
'db pool stats',
|
|
78
|
-
)
|
|
79
|
-
}, 10000)
|
|
80
|
-
|
|
81
|
-
this.ac.signal.addEventListener('abort', () => {
|
|
82
|
-
clearInterval(dbStatsInterval)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
await this.setupAppEvents()
|
|
86
|
-
this.server.listen(this.ctx.cfg.service.port)
|
|
87
|
-
this.server.keepAliveTimeout = 90000
|
|
88
|
-
await events.once(this.server, 'listening')
|
|
89
|
-
return this.server
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async destroy(): Promise<void> {
|
|
93
|
-
if (this.state === 'destroyed') return
|
|
94
|
-
this.state = 'destroyed'
|
|
95
|
-
this.ac.abort()
|
|
96
|
-
try {
|
|
97
|
-
await this.terminator.terminate()
|
|
98
|
-
} finally {
|
|
99
|
-
await this.ctx.db.close()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async setupAppEvents() {
|
|
104
|
-
const conn = await this.ctx.db.pool.connect()
|
|
105
|
-
this.ac.signal.addEventListener('abort', () => conn.release(), {
|
|
106
|
-
once: true,
|
|
107
|
-
})
|
|
108
|
-
// if these error, unhandled rejection should cause process to exit
|
|
109
|
-
conn.query(`listen ${createMuteOpChannel}`)
|
|
110
|
-
conn.query(`listen ${createNotifOpChannel}`)
|
|
111
|
-
conn.query(`listen ${createOperationChannel}`)
|
|
112
|
-
conn.on('notification', (notif) => {
|
|
113
|
-
if (notif.channel === createMuteOpChannel) {
|
|
114
|
-
this.ctx.events.emit(createMuteOpChannel)
|
|
115
|
-
}
|
|
116
|
-
if (notif.channel === createNotifOpChannel) {
|
|
117
|
-
this.ctx.events.emit(createNotifOpChannel)
|
|
118
|
-
}
|
|
119
|
-
if (notif.channel === createOperationChannel) {
|
|
120
|
-
this.ctx.events.emit(createOperationChannel)
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export default BsyncService
|
|
127
|
-
|
|
128
|
-
const isHealth = (urlStr: string | undefined) => {
|
|
129
|
-
if (!urlStr) return false
|
|
130
|
-
const url = new URL(urlStr, 'http://host')
|
|
131
|
-
return url.pathname === '/_health'
|
|
132
|
-
}
|
package/src/logger.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { type IncomingMessage } from 'node:http'
|
|
2
|
-
import { pinoHttp, stdSerializers } from 'pino-http'
|
|
3
|
-
import { obfuscateHeaders, subsystemLogger } from '@atproto/common'
|
|
4
|
-
|
|
5
|
-
export const dbLogger: ReturnType<typeof subsystemLogger> =
|
|
6
|
-
subsystemLogger('bsync:db')
|
|
7
|
-
export const httpLogger: ReturnType<typeof subsystemLogger> =
|
|
8
|
-
subsystemLogger('bsync')
|
|
9
|
-
|
|
10
|
-
export const loggerMiddleware = pinoHttp({
|
|
11
|
-
logger: httpLogger,
|
|
12
|
-
redact: {
|
|
13
|
-
paths: ['req.headers.authorization'],
|
|
14
|
-
},
|
|
15
|
-
serializers: {
|
|
16
|
-
err: (err: unknown) => ({
|
|
17
|
-
code: err?.['code'],
|
|
18
|
-
message: err?.['message'],
|
|
19
|
-
}),
|
|
20
|
-
req: (req: IncomingMessage) => {
|
|
21
|
-
const serialized = stdSerializers.req(req)
|
|
22
|
-
const headers = obfuscateHeaders(serialized.headers)
|
|
23
|
-
return { ...serialized, headers }
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
})
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
|
|
2
|
-
import { sql } from 'kysely'
|
|
3
|
-
import { AtUri } from '@atproto/syntax'
|
|
4
|
-
import { AppContext } from '../context.js'
|
|
5
|
-
import { Database } from '../db/index.js'
|
|
6
|
-
import { createMuteOpChannel } from '../db/schema/mute_op.js'
|
|
7
|
-
import { Service } from '../proto/bsync_connect.js'
|
|
8
|
-
import {
|
|
9
|
-
AddMuteOperationResponse,
|
|
10
|
-
MuteOperation_Type,
|
|
11
|
-
} from '../proto/bsync_pb.js'
|
|
12
|
-
import { authWithApiKey } from './auth.js'
|
|
13
|
-
import { isValidAtUri, isValidDid } from './util.js'
|
|
14
|
-
|
|
15
|
-
export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
|
|
16
|
-
async addMuteOperation(req, handlerCtx) {
|
|
17
|
-
authWithApiKey(ctx, handlerCtx)
|
|
18
|
-
const { db } = ctx
|
|
19
|
-
const op = validMuteOp(req)
|
|
20
|
-
const id = await db.transaction(async (txn) => {
|
|
21
|
-
// create mute op
|
|
22
|
-
const id = await createMuteOp(txn, op)
|
|
23
|
-
// update mute state
|
|
24
|
-
if (op.type === MuteOperation_Type.ADD) {
|
|
25
|
-
await addMuteItem(txn, id, op)
|
|
26
|
-
} else if (op.type === MuteOperation_Type.REMOVE) {
|
|
27
|
-
await removeMuteItem(txn, op)
|
|
28
|
-
} else if (op.type === MuteOperation_Type.CLEAR) {
|
|
29
|
-
await clearMuteItems(txn, op)
|
|
30
|
-
} else {
|
|
31
|
-
const exhaustiveCheck: never = op.type
|
|
32
|
-
throw new Error(`unreachable: ${exhaustiveCheck}`)
|
|
33
|
-
}
|
|
34
|
-
return id
|
|
35
|
-
})
|
|
36
|
-
return new AddMuteOperationResponse({
|
|
37
|
-
operation: {
|
|
38
|
-
id: String(id),
|
|
39
|
-
type: op.type,
|
|
40
|
-
actorDid: op.actorDid,
|
|
41
|
-
subject: op.subject,
|
|
42
|
-
},
|
|
43
|
-
})
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const createMuteOp = async (db: Database, op: MuteOpInfo) => {
|
|
48
|
-
const { ref } = db.db.dynamic
|
|
49
|
-
const { id } = await db.db
|
|
50
|
-
.insertInto('mute_op')
|
|
51
|
-
.values({
|
|
52
|
-
type: op.type,
|
|
53
|
-
actorDid: op.actorDid,
|
|
54
|
-
subject: op.subject,
|
|
55
|
-
})
|
|
56
|
-
.returning('id')
|
|
57
|
-
.executeTakeFirstOrThrow()
|
|
58
|
-
await sql`notify ${ref(createMuteOpChannel)}`.execute(db.db) // emitted transactionally
|
|
59
|
-
return id
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const addMuteItem = async (db: Database, fromId: number, op: MuteOpInfo) => {
|
|
63
|
-
const { ref } = db.db.dynamic
|
|
64
|
-
await db.db
|
|
65
|
-
.insertInto('mute_item')
|
|
66
|
-
.values({
|
|
67
|
-
actorDid: op.actorDid,
|
|
68
|
-
subject: op.subject,
|
|
69
|
-
fromId,
|
|
70
|
-
})
|
|
71
|
-
.onConflict((oc) =>
|
|
72
|
-
oc
|
|
73
|
-
.constraint('mute_item_pkey')
|
|
74
|
-
.doUpdateSet({ fromId: sql`${ref('excluded.fromId')}` }),
|
|
75
|
-
)
|
|
76
|
-
.execute()
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const removeMuteItem = async (db: Database, op: MuteOpInfo) => {
|
|
80
|
-
await db.db
|
|
81
|
-
.deleteFrom('mute_item')
|
|
82
|
-
.where('actorDid', '=', op.actorDid)
|
|
83
|
-
.where('subject', '=', op.subject)
|
|
84
|
-
.execute()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const clearMuteItems = async (db: Database, op: MuteOpInfo) => {
|
|
88
|
-
await db.db
|
|
89
|
-
.deleteFrom('mute_item')
|
|
90
|
-
.where('actorDid', '=', op.actorDid)
|
|
91
|
-
.execute()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const validMuteOp = (op: MuteOpInfo): MuteOpInfoValid => {
|
|
95
|
-
if (!Object.values(MuteOperation_Type).includes(op.type)) {
|
|
96
|
-
throw new ConnectError('bad mute operation type', Code.InvalidArgument)
|
|
97
|
-
}
|
|
98
|
-
if (op.type === MuteOperation_Type.UNSPECIFIED) {
|
|
99
|
-
throw new ConnectError(
|
|
100
|
-
'unspecified mute operation type',
|
|
101
|
-
Code.InvalidArgument,
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
if (!isValidDid(op.actorDid)) {
|
|
105
|
-
throw new ConnectError(
|
|
106
|
-
'actor_did must be a valid did',
|
|
107
|
-
Code.InvalidArgument,
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
if (op.type === MuteOperation_Type.CLEAR) {
|
|
111
|
-
if (op.subject !== '') {
|
|
112
|
-
throw new ConnectError(
|
|
113
|
-
'subject must not be set on a clear op',
|
|
114
|
-
Code.InvalidArgument,
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
if (isValidDid(op.subject)) {
|
|
119
|
-
// all good
|
|
120
|
-
} else if (isValidAtUri(op.subject)) {
|
|
121
|
-
const uri = new AtUri(op.subject)
|
|
122
|
-
if (
|
|
123
|
-
uri.collection !== 'app.bsky.graph.list' &&
|
|
124
|
-
uri.collection !== 'app.bsky.feed.post'
|
|
125
|
-
) {
|
|
126
|
-
throw new ConnectError(
|
|
127
|
-
'subject aturis must reference a list or post record',
|
|
128
|
-
Code.InvalidArgument,
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
throw new ConnectError(
|
|
133
|
-
'subject must be a did or aturi on add or remove op',
|
|
134
|
-
Code.InvalidArgument,
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return op as MuteOpInfoValid // op.type has been checked
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
type MuteOpInfo = {
|
|
142
|
-
type: MuteOperation_Type
|
|
143
|
-
actorDid: string
|
|
144
|
-
subject: string
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
type MuteOpInfoValid = {
|
|
148
|
-
type:
|
|
149
|
-
| MuteOperation_Type.ADD
|
|
150
|
-
| MuteOperation_Type.REMOVE
|
|
151
|
-
| MuteOperation_Type.CLEAR
|
|
152
|
-
actorDid: string
|
|
153
|
-
subject: string
|
|
154
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
|
|
2
|
-
import { sql } from 'kysely'
|
|
3
|
-
import { AppContext } from '../context.js'
|
|
4
|
-
import { Database } from '../db/index.js'
|
|
5
|
-
import { createNotifOpChannel } from '../db/schema/notif_op.js'
|
|
6
|
-
import { Service } from '../proto/bsync_connect.js'
|
|
7
|
-
import { AddNotifOperationResponse } from '../proto/bsync_pb.js'
|
|
8
|
-
import { authWithApiKey } from './auth.js'
|
|
9
|
-
import { isValidDid } from './util.js'
|
|
10
|
-
|
|
11
|
-
export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
|
|
12
|
-
async addNotifOperation(req, handlerCtx) {
|
|
13
|
-
authWithApiKey(ctx, handlerCtx)
|
|
14
|
-
const { db } = ctx
|
|
15
|
-
const { actorDid, priority } = req
|
|
16
|
-
if (!isValidDid(actorDid)) {
|
|
17
|
-
throw new ConnectError(
|
|
18
|
-
'actor_did must be a valid did',
|
|
19
|
-
Code.InvalidArgument,
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
const id = await db.transaction(async (txn) => {
|
|
23
|
-
// create notif op
|
|
24
|
-
const id = await createNotifOp(txn, actorDid, priority)
|
|
25
|
-
// update notif state
|
|
26
|
-
if (priority !== undefined) {
|
|
27
|
-
await updateNotifItem(txn, id, actorDid, priority)
|
|
28
|
-
}
|
|
29
|
-
return id
|
|
30
|
-
})
|
|
31
|
-
return new AddNotifOperationResponse({
|
|
32
|
-
operation: {
|
|
33
|
-
id: String(id),
|
|
34
|
-
actorDid,
|
|
35
|
-
priority,
|
|
36
|
-
},
|
|
37
|
-
})
|
|
38
|
-
},
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const createNotifOp = async (
|
|
42
|
-
db: Database,
|
|
43
|
-
actorDid: string,
|
|
44
|
-
priority: boolean | undefined,
|
|
45
|
-
) => {
|
|
46
|
-
const { ref } = db.db.dynamic
|
|
47
|
-
const { id } = await db.db
|
|
48
|
-
.insertInto('notif_op')
|
|
49
|
-
.values({
|
|
50
|
-
actorDid,
|
|
51
|
-
priority,
|
|
52
|
-
})
|
|
53
|
-
.returning('id')
|
|
54
|
-
.executeTakeFirstOrThrow()
|
|
55
|
-
await sql`notify ${ref(createNotifOpChannel)}`.execute(db.db) // emitted transactionally
|
|
56
|
-
return id
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const updateNotifItem = async (
|
|
60
|
-
db: Database,
|
|
61
|
-
fromId: number,
|
|
62
|
-
actorDid: string,
|
|
63
|
-
priority: boolean,
|
|
64
|
-
) => {
|
|
65
|
-
const { ref } = db.db.dynamic
|
|
66
|
-
await db.db
|
|
67
|
-
.insertInto('notif_item')
|
|
68
|
-
.values({
|
|
69
|
-
actorDid,
|
|
70
|
-
priority,
|
|
71
|
-
fromId,
|
|
72
|
-
})
|
|
73
|
-
.onConflict((oc) =>
|
|
74
|
-
oc.column('actorDid').doUpdateSet({
|
|
75
|
-
priority: sql`${ref('excluded.priority')}`,
|
|
76
|
-
fromId: sql`${ref('excluded.fromId')}`,
|
|
77
|
-
}),
|
|
78
|
-
)
|
|
79
|
-
.execute()
|
|
80
|
-
}
|
package/src/routes/auth.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Code, ConnectError, HandlerContext } from '@connectrpc/connect'
|
|
2
|
-
import { AppContext } from '../context.js'
|
|
3
|
-
|
|
4
|
-
const BEARER = 'Bearer '
|
|
5
|
-
|
|
6
|
-
export const authWithApiKey = (ctx: AppContext, handlerCtx: HandlerContext) => {
|
|
7
|
-
const authorization = handlerCtx.requestHeader.get('authorization')
|
|
8
|
-
if (!authorization?.startsWith(BEARER)) {
|
|
9
|
-
throw new ConnectError('missing auth', Code.Unauthenticated)
|
|
10
|
-
}
|
|
11
|
-
const key = authorization.slice(BEARER.length)
|
|
12
|
-
if (!ctx.cfg.auth.apiKeys.has(key)) {
|
|
13
|
-
throw new ConnectError('invalid api key', Code.Unauthenticated)
|
|
14
|
-
}
|
|
15
|
-
}
|