@atproto/bsky 0.0.25 → 0.0.26
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 +7 -0
- package/buf.gen.yaml +12 -0
- package/dist/api/app/bsky/unspecced/getTaggedSuggestions.d.ts +3 -0
- package/dist/bsync.d.ts +8 -0
- package/dist/config.d.ts +20 -0
- package/dist/context.d.ts +6 -3
- package/dist/courier.d.ts +8 -0
- package/dist/db/database-schema.d.ts +2 -1
- package/dist/db/index.js +15 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20240124T023719200Z-tagged-suggestions.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/tables/tagged-suggestion.d.ts +9 -0
- package/dist/index.js +47930 -16807
- package/dist/index.js.map +3 -3
- package/dist/indexer/config.d.ts +8 -0
- package/dist/indexer/context.d.ts +3 -0
- package/dist/ingester/config.d.ts +8 -0
- package/dist/ingester/context.d.ts +3 -0
- package/dist/ingester/mute-subscription.d.ts +22 -0
- package/dist/lexicon/index.d.ts +2 -0
- package/dist/lexicon/lexicons.d.ts +48 -0
- package/dist/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.d.ts +39 -0
- package/dist/notifications.d.ts +27 -16
- package/dist/proto/bsync_connect.d.ts +25 -0
- package/dist/proto/bsync_pb.d.ts +90 -0
- package/dist/proto/courier_connect.d.ts +25 -0
- package/dist/proto/courier_pb.d.ts +91 -0
- package/dist/services/actor/index.d.ts +2 -2
- package/dist/services/indexing/index.d.ts +2 -2
- package/dist/services/util/post.d.ts +6 -6
- package/dist/util/retry.d.ts +2 -0
- package/package.json +15 -7
- package/proto/courier.proto +56 -0
- package/src/api/app/bsky/graph/muteActor.ts +32 -5
- package/src/api/app/bsky/graph/muteActorList.ts +32 -5
- package/src/api/app/bsky/graph/unmuteActor.ts +32 -5
- package/src/api/app/bsky/graph/unmuteActorList.ts +32 -5
- package/src/api/app/bsky/notification/registerPush.ts +42 -8
- package/src/api/app/bsky/unspecced/getTaggedSuggestions.ts +21 -0
- package/src/api/index.ts +2 -0
- package/src/bsync.ts +41 -0
- package/src/config.ts +79 -0
- package/src/context.ts +12 -6
- package/src/courier.ts +41 -0
- package/src/db/database-schema.ts +2 -0
- package/src/db/migrations/20240124T023719200Z-tagged-suggestions.ts +15 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/tables/tagged-suggestion.ts +11 -0
- package/src/index.ts +26 -3
- package/src/indexer/config.ts +36 -0
- package/src/indexer/context.ts +6 -0
- package/src/indexer/index.ts +27 -3
- package/src/ingester/config.ts +34 -0
- package/src/ingester/context.ts +6 -0
- package/src/ingester/index.ts +18 -0
- package/src/ingester/mute-subscription.ts +213 -0
- package/src/lexicon/index.ts +12 -0
- package/src/lexicon/lexicons.ts +50 -0
- package/src/lexicon/types/app/bsky/unspecced/getTaggedSuggestions.ts +65 -0
- package/src/notifications.ts +165 -149
- package/src/proto/bsync_connect.ts +54 -0
- package/src/proto/bsync_pb.ts +459 -0
- package/src/proto/courier_connect.ts +50 -0
- package/src/proto/courier_pb.ts +473 -0
- package/src/services/actor/index.ts +17 -2
- package/src/services/indexing/processor.ts +1 -1
- package/src/util/retry.ts +12 -0
- package/tests/notification-server.test.ts +59 -19
- package/tests/subscription/mutes.test.ts +170 -0
- package/tests/views/suggestions.test.ts +22 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { PrimaryDatabase } from '../db'
|
|
3
|
+
import { Redis } from '../redis'
|
|
4
|
+
import { BsyncClient, Code, isBsyncError } from '../bsync'
|
|
5
|
+
import { MuteOperation, MuteOperation_Type } from '../proto/bsync_pb'
|
|
6
|
+
import logger from './logger'
|
|
7
|
+
import { wait } from '@atproto/common'
|
|
8
|
+
import {
|
|
9
|
+
AtUri,
|
|
10
|
+
InvalidDidError,
|
|
11
|
+
ensureValidAtUri,
|
|
12
|
+
ensureValidDid,
|
|
13
|
+
} from '@atproto/syntax'
|
|
14
|
+
import { ids } from '../lexicon/lexicons'
|
|
15
|
+
|
|
16
|
+
const CURSOR_KEY = 'ingester:mute:cursor'
|
|
17
|
+
|
|
18
|
+
export class MuteSubscription {
|
|
19
|
+
ac = new AbortController()
|
|
20
|
+
running: Promise<void> | undefined
|
|
21
|
+
cursor: string | null = null
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
public db: PrimaryDatabase,
|
|
25
|
+
public redis: Redis,
|
|
26
|
+
public bsyncClient: BsyncClient,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
async start() {
|
|
30
|
+
if (this.running) return
|
|
31
|
+
this.ac = new AbortController()
|
|
32
|
+
this.running = this.run()
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
// allow this to cause an unhandled rejection, let deployment handle the crash.
|
|
35
|
+
logger.error({ err }, 'mute subscription crashed')
|
|
36
|
+
throw err
|
|
37
|
+
})
|
|
38
|
+
.finally(() => (this.running = undefined))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async run() {
|
|
42
|
+
this.cursor = await this.getCursor()
|
|
43
|
+
while (!this.ac.signal.aborted) {
|
|
44
|
+
try {
|
|
45
|
+
// get page of mute ops, long-polling
|
|
46
|
+
const page = await this.bsyncClient.scanMuteOperations(
|
|
47
|
+
{
|
|
48
|
+
limit: 100,
|
|
49
|
+
cursor: this.cursor ?? undefined,
|
|
50
|
+
},
|
|
51
|
+
{ signal: this.ac.signal },
|
|
52
|
+
)
|
|
53
|
+
if (!page.cursor) {
|
|
54
|
+
throw new BadResponseError('cursor is missing')
|
|
55
|
+
}
|
|
56
|
+
// process
|
|
57
|
+
const now = new Date()
|
|
58
|
+
for (const op of page.operations) {
|
|
59
|
+
if (this.ac.signal.aborted) return
|
|
60
|
+
if (op.type === MuteOperation_Type.ADD) {
|
|
61
|
+
await this.handleAddOp(op, now)
|
|
62
|
+
} else if (op.type === MuteOperation_Type.REMOVE) {
|
|
63
|
+
await this.handleRemoveOp(op)
|
|
64
|
+
} else if (op.type === MuteOperation_Type.CLEAR) {
|
|
65
|
+
await this.handleClearOp(op)
|
|
66
|
+
} else {
|
|
67
|
+
logger.warn(
|
|
68
|
+
{ id: op.id, type: op.type },
|
|
69
|
+
'unknown mute subscription op type',
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// update cursor
|
|
74
|
+
await this.setCursor(page.cursor)
|
|
75
|
+
this.cursor = page.cursor
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (isBsyncError(err, Code.Canceled)) {
|
|
78
|
+
return // canceled, probably from destroy()
|
|
79
|
+
}
|
|
80
|
+
if (err instanceof BadResponseError) {
|
|
81
|
+
logger.warn({ err }, 'bad response from bsync')
|
|
82
|
+
} else {
|
|
83
|
+
logger.error({ err }, 'unexpected error processing mute subscription')
|
|
84
|
+
}
|
|
85
|
+
await wait(1000) // wait a second before trying again
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async handleAddOp(op: MuteOperation, createdAt: Date) {
|
|
91
|
+
assert(op.type === MuteOperation_Type.ADD)
|
|
92
|
+
if (!isValidDid(op.actorDid)) {
|
|
93
|
+
logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (isValidDid(op.subject)) {
|
|
97
|
+
await this.db.db
|
|
98
|
+
.insertInto('mute')
|
|
99
|
+
.values({
|
|
100
|
+
subjectDid: op.subject,
|
|
101
|
+
mutedByDid: op.actorDid,
|
|
102
|
+
createdAt: createdAt.toISOString(),
|
|
103
|
+
})
|
|
104
|
+
.onConflict((oc) => oc.doNothing())
|
|
105
|
+
.execute()
|
|
106
|
+
} else {
|
|
107
|
+
const listUri = isValidAtUri(op.subject)
|
|
108
|
+
? new AtUri(op.subject)
|
|
109
|
+
: undefined
|
|
110
|
+
if (listUri?.collection !== ids.AppBskyGraphList) {
|
|
111
|
+
logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
await this.db.db
|
|
115
|
+
.insertInto('list_mute')
|
|
116
|
+
.values({
|
|
117
|
+
listUri: op.subject,
|
|
118
|
+
mutedByDid: op.actorDid,
|
|
119
|
+
createdAt: createdAt.toISOString(),
|
|
120
|
+
})
|
|
121
|
+
.onConflict((oc) => oc.doNothing())
|
|
122
|
+
.execute()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async handleRemoveOp(op: MuteOperation) {
|
|
127
|
+
assert(op.type === MuteOperation_Type.REMOVE)
|
|
128
|
+
if (!isValidDid(op.actorDid)) {
|
|
129
|
+
logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (isValidDid(op.subject)) {
|
|
133
|
+
await this.db.db
|
|
134
|
+
.deleteFrom('mute')
|
|
135
|
+
.where('subjectDid', '=', op.subject)
|
|
136
|
+
.where('mutedByDid', '=', op.actorDid)
|
|
137
|
+
.execute()
|
|
138
|
+
} else {
|
|
139
|
+
const listUri = isValidAtUri(op.subject)
|
|
140
|
+
? new AtUri(op.subject)
|
|
141
|
+
: undefined
|
|
142
|
+
if (listUri?.collection !== ids.AppBskyGraphList) {
|
|
143
|
+
logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
await this.db.db
|
|
147
|
+
.deleteFrom('list_mute')
|
|
148
|
+
.where('listUri', '=', op.subject)
|
|
149
|
+
.where('mutedByDid', '=', op.actorDid)
|
|
150
|
+
.execute()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async handleClearOp(op: MuteOperation) {
|
|
155
|
+
assert(op.type === MuteOperation_Type.CLEAR)
|
|
156
|
+
if (!isValidDid(op.actorDid)) {
|
|
157
|
+
logger.warn({ id: op.id, type: op.type }, 'bad actor in mute op')
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (op.subject) {
|
|
161
|
+
logger.warn({ id: op.id, type: op.type }, 'bad subject in mute op')
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
await this.db.db
|
|
165
|
+
.deleteFrom('mute')
|
|
166
|
+
.where('mutedByDid', '=', op.actorDid)
|
|
167
|
+
.execute()
|
|
168
|
+
await this.db.db
|
|
169
|
+
.deleteFrom('list_mute')
|
|
170
|
+
.where('mutedByDid', '=', op.actorDid)
|
|
171
|
+
.execute()
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async getCursor(): Promise<string | null> {
|
|
175
|
+
return await this.redis.get(CURSOR_KEY)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async setCursor(cursor: string): Promise<void> {
|
|
179
|
+
await this.redis.set(CURSOR_KEY, cursor)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async destroy() {
|
|
183
|
+
this.ac.abort()
|
|
184
|
+
await this.running
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get destroyed() {
|
|
188
|
+
return this.ac.signal.aborted
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class BadResponseError extends Error {}
|
|
193
|
+
|
|
194
|
+
const isValidDid = (did: string) => {
|
|
195
|
+
try {
|
|
196
|
+
ensureValidDid(did)
|
|
197
|
+
return true
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (err instanceof InvalidDidError) {
|
|
200
|
+
return false
|
|
201
|
+
}
|
|
202
|
+
throw err
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const isValidAtUri = (uri: string) => {
|
|
207
|
+
try {
|
|
208
|
+
ensureValidAtUri(uri)
|
|
209
|
+
return true
|
|
210
|
+
} catch {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/lexicon/index.ts
CHANGED
|
@@ -124,6 +124,7 @@ import * as AppBskyNotificationListNotifications from './types/app/bsky/notifica
|
|
|
124
124
|
import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush'
|
|
125
125
|
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen'
|
|
126
126
|
import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators'
|
|
127
|
+
import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecced/getTaggedSuggestions'
|
|
127
128
|
import * as AppBskyUnspeccedGetTimelineSkeleton from './types/app/bsky/unspecced/getTimelineSkeleton'
|
|
128
129
|
import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton'
|
|
129
130
|
import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton'
|
|
@@ -1613,6 +1614,17 @@ export class AppBskyUnspeccedNS {
|
|
|
1613
1614
|
return this._server.xrpc.method(nsid, cfg)
|
|
1614
1615
|
}
|
|
1615
1616
|
|
|
1617
|
+
getTaggedSuggestions<AV extends AuthVerifier>(
|
|
1618
|
+
cfg: ConfigOf<
|
|
1619
|
+
AV,
|
|
1620
|
+
AppBskyUnspeccedGetTaggedSuggestions.Handler<ExtractAuth<AV>>,
|
|
1621
|
+
AppBskyUnspeccedGetTaggedSuggestions.HandlerReqCtx<ExtractAuth<AV>>
|
|
1622
|
+
>,
|
|
1623
|
+
) {
|
|
1624
|
+
const nsid = 'app.bsky.unspecced.getTaggedSuggestions' // @ts-ignore
|
|
1625
|
+
return this._server.xrpc.method(nsid, cfg)
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1616
1628
|
getTimelineSkeleton<AV extends AuthVerifier>(
|
|
1617
1629
|
cfg: ConfigOf<
|
|
1618
1630
|
AV,
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -7908,6 +7908,54 @@ export const schemaDict = {
|
|
|
7908
7908
|
},
|
|
7909
7909
|
},
|
|
7910
7910
|
},
|
|
7911
|
+
AppBskyUnspeccedGetTaggedSuggestions: {
|
|
7912
|
+
lexicon: 1,
|
|
7913
|
+
id: 'app.bsky.unspecced.getTaggedSuggestions',
|
|
7914
|
+
defs: {
|
|
7915
|
+
main: {
|
|
7916
|
+
type: 'query',
|
|
7917
|
+
description:
|
|
7918
|
+
'Get a list of suggestions (feeds and users) tagged with categories',
|
|
7919
|
+
parameters: {
|
|
7920
|
+
type: 'params',
|
|
7921
|
+
properties: {},
|
|
7922
|
+
},
|
|
7923
|
+
output: {
|
|
7924
|
+
encoding: 'application/json',
|
|
7925
|
+
schema: {
|
|
7926
|
+
type: 'object',
|
|
7927
|
+
required: ['suggestions'],
|
|
7928
|
+
properties: {
|
|
7929
|
+
suggestions: {
|
|
7930
|
+
type: 'array',
|
|
7931
|
+
items: {
|
|
7932
|
+
type: 'ref',
|
|
7933
|
+
ref: 'lex:app.bsky.unspecced.getTaggedSuggestions#suggestion',
|
|
7934
|
+
},
|
|
7935
|
+
},
|
|
7936
|
+
},
|
|
7937
|
+
},
|
|
7938
|
+
},
|
|
7939
|
+
},
|
|
7940
|
+
suggestion: {
|
|
7941
|
+
type: 'object',
|
|
7942
|
+
required: ['tag', 'subjectType', 'subject'],
|
|
7943
|
+
properties: {
|
|
7944
|
+
tag: {
|
|
7945
|
+
type: 'string',
|
|
7946
|
+
},
|
|
7947
|
+
subjectType: {
|
|
7948
|
+
type: 'string',
|
|
7949
|
+
knownValues: ['actor', 'feed'],
|
|
7950
|
+
},
|
|
7951
|
+
subject: {
|
|
7952
|
+
type: 'string',
|
|
7953
|
+
format: 'uri',
|
|
7954
|
+
},
|
|
7955
|
+
},
|
|
7956
|
+
},
|
|
7957
|
+
},
|
|
7958
|
+
},
|
|
7911
7959
|
AppBskyUnspeccedGetTimelineSkeleton: {
|
|
7912
7960
|
lexicon: 1,
|
|
7913
7961
|
id: 'app.bsky.unspecced.getTimelineSkeleton',
|
|
@@ -8242,6 +8290,8 @@ export const ids = {
|
|
|
8242
8290
|
AppBskyUnspeccedDefs: 'app.bsky.unspecced.defs',
|
|
8243
8291
|
AppBskyUnspeccedGetPopularFeedGenerators:
|
|
8244
8292
|
'app.bsky.unspecced.getPopularFeedGenerators',
|
|
8293
|
+
AppBskyUnspeccedGetTaggedSuggestions:
|
|
8294
|
+
'app.bsky.unspecced.getTaggedSuggestions',
|
|
8245
8295
|
AppBskyUnspeccedGetTimelineSkeleton: 'app.bsky.unspecced.getTimelineSkeleton',
|
|
8246
8296
|
AppBskyUnspeccedSearchActorsSkeleton:
|
|
8247
8297
|
'app.bsky.unspecced.searchActorsSkeleton',
|
|
@@ -0,0 +1,65 @@
|
|
|
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 } from '@atproto/xrpc-server'
|
|
10
|
+
|
|
11
|
+
export interface QueryParams {}
|
|
12
|
+
|
|
13
|
+
export type InputSchema = undefined
|
|
14
|
+
|
|
15
|
+
export interface OutputSchema {
|
|
16
|
+
suggestions: Suggestion[]
|
|
17
|
+
[k: string]: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type HandlerInput = undefined
|
|
21
|
+
|
|
22
|
+
export interface HandlerSuccess {
|
|
23
|
+
encoding: 'application/json'
|
|
24
|
+
body: OutputSchema
|
|
25
|
+
headers?: { [key: string]: string }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface HandlerError {
|
|
29
|
+
status: number
|
|
30
|
+
message?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type HandlerOutput = HandlerError | HandlerSuccess
|
|
34
|
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
|
|
35
|
+
auth: HA
|
|
36
|
+
params: QueryParams
|
|
37
|
+
input: HandlerInput
|
|
38
|
+
req: express.Request
|
|
39
|
+
res: express.Response
|
|
40
|
+
}
|
|
41
|
+
export type Handler<HA extends HandlerAuth = never> = (
|
|
42
|
+
ctx: HandlerReqCtx<HA>,
|
|
43
|
+
) => Promise<HandlerOutput> | HandlerOutput
|
|
44
|
+
|
|
45
|
+
export interface Suggestion {
|
|
46
|
+
tag: string
|
|
47
|
+
subjectType: 'actor' | 'feed' | (string & {})
|
|
48
|
+
subject: string
|
|
49
|
+
[k: string]: unknown
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isSuggestion(v: unknown): v is Suggestion {
|
|
53
|
+
return (
|
|
54
|
+
isObj(v) &&
|
|
55
|
+
hasProp(v, '$type') &&
|
|
56
|
+
v.$type === 'app.bsky.unspecced.getTaggedSuggestions#suggestion'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateSuggestion(v: unknown): ValidationResult {
|
|
61
|
+
return lexicons.validate(
|
|
62
|
+
'app.bsky.unspecced.getTaggedSuggestions#suggestion',
|
|
63
|
+
v,
|
|
64
|
+
)
|
|
65
|
+
}
|