@atproto/bsky 0.0.16 → 0.0.17
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/dist/cache/read-through.d.ts +30 -0
- package/dist/config.d.ts +18 -0
- package/dist/context.d.ts +6 -6
- package/dist/daemon/config.d.ts +15 -0
- package/dist/daemon/context.d.ts +15 -0
- package/dist/daemon/index.d.ts +23 -0
- package/dist/daemon/logger.d.ts +3 -0
- package/dist/daemon/notifications.d.ts +18 -0
- package/dist/daemon/services.d.ts +11 -0
- package/dist/db/database-schema.d.ts +1 -2
- package/dist/db/index.js +16 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/did-cache.d.ts +10 -7
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1917 -934
- package/dist/index.js.map +3 -3
- package/dist/indexer/context.d.ts +2 -0
- package/dist/indexer/index.d.ts +1 -0
- package/dist/lexicon/index.d.ts +12 -0
- package/dist/lexicon/lexicons.d.ts +134 -0
- package/dist/lexicon/types/com/atproto/admin/deleteAccount.d.ts +25 -0
- package/dist/lexicon/types/com/atproto/temp/importRepo.d.ts +32 -0
- package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
- package/dist/lexicon/types/com/atproto/temp/transferAccount.d.ts +42 -0
- package/dist/logger.d.ts +1 -0
- package/dist/redis.d.ts +10 -1
- package/dist/services/actor/index.d.ts +18 -4
- package/dist/services/actor/views.d.ts +4 -3
- package/dist/services/feed/index.d.ts +6 -4
- package/dist/services/feed/views.d.ts +5 -4
- package/dist/services/index.d.ts +3 -7
- package/dist/services/label/index.d.ts +10 -4
- package/dist/services/moderation/index.d.ts +0 -1
- package/dist/services/types.d.ts +3 -0
- package/dist/services/util/notification.d.ts +5 -0
- package/dist/services/util/post.d.ts +6 -6
- package/dist/util/retry.d.ts +1 -6
- package/package.json +5 -5
- package/src/cache/read-through.ts +151 -0
- package/src/config.ts +90 -1
- package/src/context.ts +7 -7
- package/src/daemon/config.ts +60 -0
- package/src/daemon/context.ts +27 -0
- package/src/daemon/index.ts +78 -0
- package/src/daemon/logger.ts +6 -0
- package/src/daemon/notifications.ts +54 -0
- package/src/daemon/services.ts +22 -0
- package/src/db/database-schema.ts +0 -2
- package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/did-cache.ts +33 -56
- package/src/feed-gen/index.ts +0 -4
- package/src/index.ts +55 -16
- package/src/indexer/context.ts +5 -0
- package/src/indexer/index.ts +10 -7
- package/src/lexicon/index.ts +50 -0
- package/src/lexicon/lexicons.ts +156 -0
- package/src/lexicon/types/com/atproto/admin/deleteAccount.ts +38 -0
- package/src/lexicon/types/com/atproto/temp/importRepo.ts +45 -0
- package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
- package/src/lexicon/types/com/atproto/temp/transferAccount.ts +62 -0
- package/src/logger.ts +2 -0
- package/src/redis.ts +43 -3
- package/src/services/actor/index.ts +55 -7
- package/src/services/actor/views.ts +13 -7
- package/src/services/feed/index.ts +27 -13
- package/src/services/feed/views.ts +20 -10
- package/src/services/index.ts +14 -14
- package/src/services/indexing/index.ts +7 -10
- package/src/services/indexing/plugins/post.ts +13 -0
- package/src/services/label/index.ts +66 -22
- package/src/services/moderation/index.ts +1 -1
- package/src/services/moderation/status.ts +1 -4
- package/src/services/types.ts +4 -0
- package/src/services/util/notification.ts +70 -0
- package/src/util/retry.ts +1 -44
- package/tests/admin/get-repo.test.ts +5 -3
- package/tests/admin/moderation.test.ts +2 -2
- package/tests/admin/repo-search.test.ts +1 -0
- package/tests/algos/hot-classic.test.ts +1 -2
- package/tests/auth.test.ts +1 -1
- package/tests/auto-moderator/labeler.test.ts +19 -20
- package/tests/auto-moderator/takedowns.test.ts +16 -10
- package/tests/blob-resolver.test.ts +4 -2
- package/tests/daemon.test.ts +191 -0
- package/tests/did-cache.test.ts +20 -5
- package/tests/handle-invalidation.test.ts +1 -5
- package/tests/indexing.test.ts +20 -13
- package/tests/redis-cache.test.ts +231 -0
- package/tests/seeds/basic.ts +3 -0
- package/tests/subscription/repo.test.ts +4 -7
- package/tests/views/profile.test.ts +0 -1
- package/tests/views/thread.test.ts +73 -78
- package/tests/views/threadgating.test.ts +38 -0
- package/dist/db/tables/did-cache.d.ts +0 -10
- package/dist/feed-gen/best-of-follows.d.ts +0 -29
- package/dist/feed-gen/whats-hot.d.ts +0 -29
- package/dist/feed-gen/with-friends.d.ts +0 -3
- package/dist/label-cache.d.ts +0 -19
- package/src/db/tables/did-cache.ts +0 -13
- package/src/feed-gen/best-of-follows.ts +0 -77
- package/src/feed-gen/whats-hot.ts +0 -101
- package/src/feed-gen/with-friends.ts +0 -43
- package/src/label-cache.ts +0 -90
- package/tests/algos/whats-hot.test.ts +0 -118
- package/tests/algos/with-friends.test.ts +0 -145
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mapDefined } from '@atproto/common'
|
|
2
|
+
import { AtUri } from '@atproto/syntax'
|
|
2
3
|
import { Database } from '../../db'
|
|
3
4
|
import {
|
|
4
5
|
FeedViewPost,
|
|
@@ -37,26 +38,35 @@ import {
|
|
|
37
38
|
} from './types'
|
|
38
39
|
import { Labels, getSelfLabels } from '../label'
|
|
39
40
|
import { ImageUriBuilder } from '../../image/uri'
|
|
40
|
-
import { LabelCache } from '../../label-cache'
|
|
41
41
|
import { ActorInfoMap, ActorService } from '../actor'
|
|
42
42
|
import { ListInfoMap, GraphService } from '../graph'
|
|
43
|
-
import {
|
|
43
|
+
import { FromDb } from '../types'
|
|
44
44
|
import { parseThreadGate } from './util'
|
|
45
45
|
|
|
46
46
|
export class FeedViews {
|
|
47
|
+
services: {
|
|
48
|
+
actor: ActorService
|
|
49
|
+
graph: GraphService
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
constructor(
|
|
48
53
|
public db: Database,
|
|
49
54
|
public imgUriBuilder: ImageUriBuilder,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
private actor: FromDb<ActorService>,
|
|
56
|
+
private graph: FromDb<GraphService>,
|
|
57
|
+
) {
|
|
58
|
+
this.services = {
|
|
59
|
+
actor: actor(this.db),
|
|
60
|
+
graph: graph(this.db),
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
static creator(
|
|
65
|
+
imgUriBuilder: ImageUriBuilder,
|
|
66
|
+
actor: FromDb<ActorService>,
|
|
67
|
+
graph: FromDb<GraphService>,
|
|
68
|
+
) {
|
|
69
|
+
return (db: Database) => new FeedViews(db, imgUriBuilder, actor, graph)
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
formatFeedGeneratorView(
|
package/src/services/index.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { Database, PrimaryDatabase } from '../db'
|
|
2
1
|
import { ImageUriBuilder } from '../image/uri'
|
|
3
2
|
import { ActorService } from './actor'
|
|
4
3
|
import { FeedService } from './feed'
|
|
5
4
|
import { GraphService } from './graph'
|
|
6
5
|
import { ModerationService } from './moderation'
|
|
7
|
-
import { LabelService } from './label'
|
|
6
|
+
import { LabelCacheOpts, LabelService } from './label'
|
|
8
7
|
import { ImageInvalidator } from '../image/invalidator'
|
|
9
|
-
import {
|
|
8
|
+
import { FromDb, FromDbPrimary } from './types'
|
|
10
9
|
|
|
11
10
|
export function createServices(resources: {
|
|
12
11
|
imgUriBuilder: ImageUriBuilder
|
|
13
12
|
imgInvalidator: ImageInvalidator
|
|
14
|
-
|
|
13
|
+
labelCacheOpts: LabelCacheOpts
|
|
15
14
|
}): Services {
|
|
16
|
-
const { imgUriBuilder, imgInvalidator,
|
|
15
|
+
const { imgUriBuilder, imgInvalidator, labelCacheOpts } = resources
|
|
16
|
+
const label = LabelService.creator(labelCacheOpts)
|
|
17
|
+
const graph = GraphService.creator(imgUriBuilder)
|
|
18
|
+
const actor = ActorService.creator(imgUriBuilder, graph, label)
|
|
19
|
+
const moderation = ModerationService.creator(imgUriBuilder, imgInvalidator)
|
|
20
|
+
const feed = FeedService.creator(imgUriBuilder, actor, label, graph)
|
|
17
21
|
return {
|
|
18
|
-
actor
|
|
19
|
-
feed
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
label
|
|
22
|
+
actor,
|
|
23
|
+
feed,
|
|
24
|
+
moderation,
|
|
25
|
+
graph,
|
|
26
|
+
label,
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
|
|
@@ -30,7 +34,3 @@ export type Services = {
|
|
|
30
34
|
moderation: FromDbPrimary<ModerationService>
|
|
31
35
|
label: FromDb<LabelService>
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
type FromDb<T> = (db: Database) => T
|
|
35
|
-
|
|
36
|
-
type FromDbPrimary<T> = (db: PrimaryDatabase) => T
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
verifyRepo,
|
|
8
8
|
Commit,
|
|
9
9
|
VerifiedRepo,
|
|
10
|
+
getAndParseRecord,
|
|
10
11
|
} from '@atproto/repo'
|
|
11
12
|
import { AtUri } from '@atproto/syntax'
|
|
12
13
|
import { IdResolver, getPds } from '@atproto/identity'
|
|
@@ -201,10 +202,11 @@ export class IndexingService {
|
|
|
201
202
|
if (op.op === 'delete') {
|
|
202
203
|
await this.deleteRecord(uri)
|
|
203
204
|
} else {
|
|
205
|
+
const parsed = await getAndParseRecord(blocks, cid)
|
|
204
206
|
await this.indexRecord(
|
|
205
207
|
uri,
|
|
206
208
|
cid,
|
|
207
|
-
|
|
209
|
+
parsed.record,
|
|
208
210
|
op.op === 'create' ? WriteOpAction.Create : WriteOpAction.Update,
|
|
209
211
|
now,
|
|
210
212
|
)
|
|
@@ -389,19 +391,15 @@ type UriAndCid = {
|
|
|
389
391
|
cid: CID
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
type RecordDescript = UriAndCid & {
|
|
393
|
-
value: unknown
|
|
394
|
-
}
|
|
395
|
-
|
|
396
394
|
type IndexOp =
|
|
397
395
|
| ({
|
|
398
396
|
op: 'create' | 'update'
|
|
399
|
-
} &
|
|
397
|
+
} & UriAndCid)
|
|
400
398
|
| ({ op: 'delete' } & UriAndCid)
|
|
401
399
|
|
|
402
400
|
const findDiffFromCheckout = (
|
|
403
401
|
curr: Record<string, UriAndCid>,
|
|
404
|
-
checkout: Record<string,
|
|
402
|
+
checkout: Record<string, UriAndCid>,
|
|
405
403
|
): IndexOp[] => {
|
|
406
404
|
const ops: IndexOp[] = []
|
|
407
405
|
for (const uri of Object.keys(checkout)) {
|
|
@@ -428,14 +426,13 @@ const findDiffFromCheckout = (
|
|
|
428
426
|
const formatCheckout = (
|
|
429
427
|
did: string,
|
|
430
428
|
verifiedRepo: VerifiedRepo,
|
|
431
|
-
): Record<string,
|
|
432
|
-
const records: Record<string,
|
|
429
|
+
): Record<string, UriAndCid> => {
|
|
430
|
+
const records: Record<string, UriAndCid> = {}
|
|
433
431
|
for (const create of verifiedRepo.creates) {
|
|
434
432
|
const uri = AtUri.make(did, create.collection, create.rkey)
|
|
435
433
|
records[uri.toString()] = {
|
|
436
434
|
uri,
|
|
437
435
|
cid: create.cid,
|
|
438
|
-
value: create.record,
|
|
439
436
|
}
|
|
440
437
|
}
|
|
441
438
|
return records
|
|
@@ -112,6 +112,7 @@ const insertFn = async (
|
|
|
112
112
|
obj.reply,
|
|
113
113
|
)
|
|
114
114
|
if (invalidReplyRoot || violatesThreadGate) {
|
|
115
|
+
Object.assign(insertedPost, { invalidReplyRoot, violatesThreadGate })
|
|
115
116
|
await db
|
|
116
117
|
.updateTable('post')
|
|
117
118
|
.where('uri', '=', post.uri)
|
|
@@ -241,6 +242,13 @@ const notifsForInsert = (obj: IndexedPost) => {
|
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
if (obj.post.violatesThreadGate) {
|
|
246
|
+
// don't generate reply notifications when post violates threadgate
|
|
247
|
+
return notifs
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// reply notifications
|
|
251
|
+
|
|
244
252
|
for (const ancestor of obj.ancestors ?? []) {
|
|
245
253
|
if (ancestor.uri === obj.post.uri) continue // no need to notify for own post
|
|
246
254
|
if (ancestor.height < REPLY_NOTIF_DEPTH) {
|
|
@@ -353,6 +361,11 @@ const updateAggregates = async (db: DatabaseSchema, postIdx: IndexedPost) => {
|
|
|
353
361
|
replyCount: db
|
|
354
362
|
.selectFrom('post')
|
|
355
363
|
.where('post.replyParent', '=', postIdx.post.replyParent)
|
|
364
|
+
.where((qb) =>
|
|
365
|
+
qb
|
|
366
|
+
.where('post.violatesThreadGate', 'is', null)
|
|
367
|
+
.orWhere('post.violatesThreadGate', '=', false),
|
|
368
|
+
)
|
|
356
369
|
.select(countAll.as('count')),
|
|
357
370
|
})
|
|
358
371
|
.onConflict((oc) =>
|
|
@@ -3,15 +3,36 @@ import { AtUri, normalizeDatetimeAlways } from '@atproto/syntax'
|
|
|
3
3
|
import { Database } from '../../db'
|
|
4
4
|
import { Label, isSelfLabels } from '../../lexicon/types/com/atproto/label/defs'
|
|
5
5
|
import { ids } from '../../lexicon/lexicons'
|
|
6
|
-
import {
|
|
6
|
+
import { ReadThroughCache } from '../../cache/read-through'
|
|
7
|
+
import { Redis } from '../../redis'
|
|
7
8
|
|
|
8
9
|
export type Labels = Record<string, Label[]>
|
|
9
10
|
|
|
11
|
+
export type LabelCacheOpts = {
|
|
12
|
+
redis: Redis
|
|
13
|
+
staleTTL: number
|
|
14
|
+
maxTTL: number
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
export class LabelService {
|
|
11
|
-
|
|
18
|
+
public cache: ReadThroughCache<Label[]> | null
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
constructor(public db: Database, cacheOpts: LabelCacheOpts | null) {
|
|
21
|
+
if (cacheOpts) {
|
|
22
|
+
this.cache = new ReadThroughCache(cacheOpts.redis, {
|
|
23
|
+
...cacheOpts,
|
|
24
|
+
fetchMethod: async (subject: string) => {
|
|
25
|
+
const res = await fetchLabelsForSubjects(db, [subject])
|
|
26
|
+
return res[subject] ?? []
|
|
27
|
+
},
|
|
28
|
+
fetchManyMethod: (subjects: string[]) =>
|
|
29
|
+
fetchLabelsForSubjects(db, subjects),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static creator(cacheOpts: LabelCacheOpts | null) {
|
|
35
|
+
return (db: Database) => new LabelService(db, cacheOpts)
|
|
15
36
|
}
|
|
16
37
|
|
|
17
38
|
async formatAndCreate(
|
|
@@ -72,24 +93,19 @@ export class LabelService {
|
|
|
72
93
|
},
|
|
73
94
|
): Promise<Labels> {
|
|
74
95
|
if (subjects.length < 1) return {}
|
|
75
|
-
const res =
|
|
76
|
-
this.cache
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
cid: cur.cid === '' ? undefined : cur.cid,
|
|
89
|
-
neg: cur.neg,
|
|
90
|
-
})
|
|
91
|
-
return acc
|
|
92
|
-
}, {} as Labels)
|
|
96
|
+
const res = this.cache
|
|
97
|
+
? await this.cache.getMany(subjects, { revalidate: opts?.skipCache })
|
|
98
|
+
: await fetchLabelsForSubjects(this.db, subjects)
|
|
99
|
+
|
|
100
|
+
if (opts?.includeNeg) {
|
|
101
|
+
return res
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const noNegs: Labels = {}
|
|
105
|
+
for (const [key, val] of Object.entries(res)) {
|
|
106
|
+
noNegs[key] = val.filter((label) => !label.neg)
|
|
107
|
+
}
|
|
108
|
+
return noNegs
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
// gets labels for any record. when did is present, combine labels for both did & profile record.
|
|
@@ -171,3 +187,31 @@ export function getSelfLabels(details: {
|
|
|
171
187
|
return { src, uri, cid, val, cts, neg: false }
|
|
172
188
|
})
|
|
173
189
|
}
|
|
190
|
+
|
|
191
|
+
const fetchLabelsForSubjects = async (
|
|
192
|
+
db: Database,
|
|
193
|
+
subjects: string[],
|
|
194
|
+
): Promise<Record<string, Label[]>> => {
|
|
195
|
+
if (subjects.length === 0) {
|
|
196
|
+
return {}
|
|
197
|
+
}
|
|
198
|
+
const res = await db.db
|
|
199
|
+
.selectFrom('label')
|
|
200
|
+
.where('label.uri', 'in', subjects)
|
|
201
|
+
.selectAll()
|
|
202
|
+
.execute()
|
|
203
|
+
const labelMap = res.reduce((acc, cur) => {
|
|
204
|
+
acc[cur.uri] ??= []
|
|
205
|
+
acc[cur.uri].push({
|
|
206
|
+
...cur,
|
|
207
|
+
cid: cur.cid === '' ? undefined : cur.cid,
|
|
208
|
+
neg: cur.neg,
|
|
209
|
+
})
|
|
210
|
+
return acc
|
|
211
|
+
}, {} as Record<string, Label[]>)
|
|
212
|
+
// ensure we cache negatives
|
|
213
|
+
for (const subject of subjects) {
|
|
214
|
+
labelMap[subject] ??= []
|
|
215
|
+
}
|
|
216
|
+
return labelMap
|
|
217
|
+
}
|
|
@@ -639,7 +639,7 @@ export class ModerationService {
|
|
|
639
639
|
const { did, recordPath } = getStatusIdentifierFromSubject(
|
|
640
640
|
'did' in subject ? subject.did : subject.uri,
|
|
641
641
|
)
|
|
642
|
-
|
|
642
|
+
const builder = this.db.db
|
|
643
643
|
.selectFrom('moderation_subject_status')
|
|
644
644
|
.where('did', '=', did)
|
|
645
645
|
.where('recordPath', '=', recordPath || '')
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { AtUri } from '@atproto/syntax'
|
|
4
4
|
import { PrimaryDatabase } from '../../db'
|
|
5
|
-
import {
|
|
6
|
-
ModerationEvent,
|
|
7
|
-
ModerationSubjectStatus,
|
|
8
|
-
} from '../../db/tables/moderation'
|
|
5
|
+
import { ModerationSubjectStatus } from '../../db/tables/moderation'
|
|
9
6
|
import {
|
|
10
7
|
REVIEWOPEN,
|
|
11
8
|
REVIEWCLOSED,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { sql } from 'kysely'
|
|
2
|
+
import { countAll } from '../../db/util'
|
|
3
|
+
import { PrimaryDatabase } from '../../db'
|
|
4
|
+
|
|
5
|
+
// i.e. 30 days before the last time the user checked their notifs
|
|
6
|
+
export const BEFORE_LAST_SEEN_DAYS = 30
|
|
7
|
+
// i.e. 180 days before the latest unread notification
|
|
8
|
+
export const BEFORE_LATEST_UNREAD_DAYS = 180
|
|
9
|
+
// don't consider culling unreads until they hit this threshold, and then enforce beforeLatestUnreadThresholdDays
|
|
10
|
+
export const UNREAD_KEPT_COUNT = 500
|
|
11
|
+
|
|
12
|
+
export const tidyNotifications = async (db: PrimaryDatabase, did: string) => {
|
|
13
|
+
const stats = await db.db
|
|
14
|
+
.selectFrom('notification')
|
|
15
|
+
.select([
|
|
16
|
+
sql<0 | 1>`("sortAt" < "lastSeenNotifs")`.as('read'),
|
|
17
|
+
countAll.as('count'),
|
|
18
|
+
sql<string>`min("sortAt")`.as('earliestAt'),
|
|
19
|
+
sql<string>`max("sortAt")`.as('latestAt'),
|
|
20
|
+
sql<string>`max("lastSeenNotifs")`.as('lastSeenAt'),
|
|
21
|
+
])
|
|
22
|
+
.leftJoin('actor_state', 'actor_state.did', 'notification.did')
|
|
23
|
+
.where('notification.did', '=', did)
|
|
24
|
+
.groupBy(sql`1`) // group by read (i.e. 1st column)
|
|
25
|
+
.execute()
|
|
26
|
+
const readStats = stats.find((stat) => stat.read)
|
|
27
|
+
const unreadStats = stats.find((stat) => !stat.read)
|
|
28
|
+
let readCutoffAt: Date | undefined
|
|
29
|
+
let unreadCutoffAt: Date | undefined
|
|
30
|
+
if (readStats) {
|
|
31
|
+
readCutoffAt = addDays(
|
|
32
|
+
new Date(readStats.lastSeenAt),
|
|
33
|
+
-BEFORE_LAST_SEEN_DAYS,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
if (unreadStats && unreadStats.count > UNREAD_KEPT_COUNT) {
|
|
37
|
+
unreadCutoffAt = addDays(
|
|
38
|
+
new Date(unreadStats.latestAt),
|
|
39
|
+
-BEFORE_LATEST_UNREAD_DAYS,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
// take most recent of read/unread cutoffs
|
|
43
|
+
const cutoffAt = greatest(readCutoffAt, unreadCutoffAt)
|
|
44
|
+
if (cutoffAt) {
|
|
45
|
+
// skip delete if it wont catch any notifications
|
|
46
|
+
const earliestAt = least(readStats?.earliestAt, unreadStats?.earliestAt)
|
|
47
|
+
if (earliestAt && earliestAt < cutoffAt.toISOString()) {
|
|
48
|
+
await db.db
|
|
49
|
+
.deleteFrom('notification')
|
|
50
|
+
.where('did', '=', did)
|
|
51
|
+
.where('sortAt', '<', cutoffAt.toISOString())
|
|
52
|
+
.execute()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const addDays = (date: Date, days: number) => {
|
|
58
|
+
date.setDate(date.getDate() + days)
|
|
59
|
+
return date
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const least = <T extends Ordered>(a: T | undefined, b: T | undefined) => {
|
|
63
|
+
return a !== undefined && (b === undefined || a < b) ? a : b
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const greatest = <T extends Ordered>(a: T | undefined, b: T | undefined) => {
|
|
67
|
+
return a !== undefined && (b === undefined || a > b) ? a : b
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type Ordered = string | number | Date
|
package/src/util/retry.ts
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
import { AxiosError } from 'axios'
|
|
2
|
-
import { wait } from '@atproto/common'
|
|
3
2
|
import { XRPCError, ResponseType } from '@atproto/xrpc'
|
|
4
|
-
|
|
5
|
-
export async function retry<T>(
|
|
6
|
-
fn: () => Promise<T>,
|
|
7
|
-
opts: RetryOptions = {},
|
|
8
|
-
): Promise<T> {
|
|
9
|
-
const { max = 3, retryable = () => true } = opts
|
|
10
|
-
let retries = 0
|
|
11
|
-
let doneError: unknown
|
|
12
|
-
while (!doneError) {
|
|
13
|
-
try {
|
|
14
|
-
if (retries) await backoff(retries)
|
|
15
|
-
return await fn()
|
|
16
|
-
} catch (err) {
|
|
17
|
-
const willRetry = retries < max && retryable(err)
|
|
18
|
-
if (!willRetry) doneError = err
|
|
19
|
-
retries += 1
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
throw doneError
|
|
23
|
-
}
|
|
3
|
+
import { RetryOptions, retry } from '@atproto/common'
|
|
24
4
|
|
|
25
5
|
export async function retryHttp<T>(
|
|
26
6
|
fn: () => Promise<T>,
|
|
@@ -44,26 +24,3 @@ export function retryableHttp(err: unknown) {
|
|
|
44
24
|
const retryableHttpStatusCodes = new Set([
|
|
45
25
|
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
46
26
|
])
|
|
47
|
-
|
|
48
|
-
type RetryOptions = {
|
|
49
|
-
max?: number
|
|
50
|
-
retryable?: (err: unknown) => boolean
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Waits exponential backoff with max and jitter: ~50, ~100, ~200, ~400, ~800, ~1000, ~1000, ...
|
|
54
|
-
async function backoff(n: number, multiplier = 50, max = 1000) {
|
|
55
|
-
const exponentialMs = Math.pow(2, n) * multiplier
|
|
56
|
-
const ms = Math.min(exponentialMs, max)
|
|
57
|
-
await wait(jitter(ms))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Adds randomness +/-15% of value
|
|
61
|
-
function jitter(value: number) {
|
|
62
|
-
const delta = value * 0.15
|
|
63
|
-
return value + randomRange(-delta, delta)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function randomRange(from: number, to: number) {
|
|
67
|
-
const rand = Math.random() * (to - from)
|
|
68
|
-
return rand + from
|
|
69
|
-
}
|
|
@@ -97,9 +97,11 @@ describe('admin get repo view', () => {
|
|
|
97
97
|
expect(beforeEmailVerification.emailConfirmedAt).toBeUndefined()
|
|
98
98
|
const timestampBeforeVerification = Date.now()
|
|
99
99
|
const bobsAccount = sc.accounts[sc.dids.bob]
|
|
100
|
-
const verificationToken =
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
const verificationToken =
|
|
101
|
+
await network.pds.ctx.accountManager.createEmailToken(
|
|
102
|
+
sc.dids.bob,
|
|
103
|
+
'confirm_email',
|
|
104
|
+
)
|
|
103
105
|
await agent.api.com.atproto.server.confirmEmail(
|
|
104
106
|
{ email: bobsAccount.email, token: verificationToken },
|
|
105
107
|
{
|
|
@@ -387,7 +387,7 @@ describe('moderation', () => {
|
|
|
387
387
|
createLabelVals: ['nsfw'],
|
|
388
388
|
negateLabelVals: [],
|
|
389
389
|
})
|
|
390
|
-
|
|
390
|
+
await emitModEvent({
|
|
391
391
|
$type: 'com.atproto.admin.defs#modEventTakedown',
|
|
392
392
|
})
|
|
393
393
|
|
|
@@ -779,7 +779,7 @@ describe('moderation', () => {
|
|
|
779
779
|
negateLabelVals: ModEventLabel['negateLabelVals']
|
|
780
780
|
},
|
|
781
781
|
) {
|
|
782
|
-
const { createLabelVals, negateLabelVals
|
|
782
|
+
const { createLabelVals, negateLabelVals } = opts
|
|
783
783
|
const result = await agent.api.com.atproto.admin.emitModerationEvent(
|
|
784
784
|
{
|
|
785
785
|
event: {
|
|
@@ -31,7 +31,6 @@ describe('algo hot-classic', () => {
|
|
|
31
31
|
alice = sc.dids.alice
|
|
32
32
|
bob = sc.dids.bob
|
|
33
33
|
await network.processAll()
|
|
34
|
-
await network.bsky.processAll()
|
|
35
34
|
})
|
|
36
35
|
|
|
37
36
|
afterAll(async () => {
|
|
@@ -59,7 +58,7 @@ describe('algo hot-classic', () => {
|
|
|
59
58
|
await sc.like(sc.dids[name], two.ref)
|
|
60
59
|
await sc.like(sc.dids[name], three.ref)
|
|
61
60
|
}
|
|
62
|
-
await network.
|
|
61
|
+
await network.processAll()
|
|
63
62
|
|
|
64
63
|
const res = await agent.api.app.bsky.feed.getFeed(
|
|
65
64
|
{ feed: feedUri },
|
package/tests/auth.test.ts
CHANGED
|
@@ -36,7 +36,7 @@ describe('auth', () => {
|
|
|
36
36
|
{ headers: { authorization: `Bearer ${jwt}` } },
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
|
-
const origSigningKey = network.pds.ctx.
|
|
39
|
+
const origSigningKey = await network.pds.ctx.actorStore.keypair(issuer)
|
|
40
40
|
const newSigningKey = await Secp256k1Keypair.create({ exportable: true })
|
|
41
41
|
// confirm original signing key works
|
|
42
42
|
await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined()
|
|
@@ -40,26 +40,25 @@ describe('labeler', () => {
|
|
|
40
40
|
await usersSeed(sc)
|
|
41
41
|
await network.processAll()
|
|
42
42
|
alice = sc.dids.alice
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
return blobRef
|
|
43
|
+
const storeBlob = (bytes: Uint8Array) => {
|
|
44
|
+
return pdsCtx.actorStore.transact(alice, async (store) => {
|
|
45
|
+
const blobRef = await store.repo.blob.addUntetheredBlob(
|
|
46
|
+
'image/jpeg',
|
|
47
|
+
Readable.from([bytes], { objectMode: false }),
|
|
48
|
+
)
|
|
49
|
+
const preparedBlobRef = {
|
|
50
|
+
cid: blobRef.ref,
|
|
51
|
+
mimeType: 'image/jpeg',
|
|
52
|
+
constraints: {},
|
|
53
|
+
}
|
|
54
|
+
await store.repo.blob.verifyBlobAndMakePermanent(preparedBlobRef)
|
|
55
|
+
await store.repo.blob.associateBlob(
|
|
56
|
+
preparedBlobRef,
|
|
57
|
+
postUri(),
|
|
58
|
+
TID.nextStr(),
|
|
59
|
+
)
|
|
60
|
+
return blobRef
|
|
61
|
+
})
|
|
63
62
|
}
|
|
64
63
|
const bytes1 = new Uint8Array([1, 2, 3, 4])
|
|
65
64
|
const bytes2 = new Uint8Array([5, 6, 7, 8])
|
|
@@ -106,11 +106,15 @@ describe('takedowner', () => {
|
|
|
106
106
|
.executeTakeFirst()
|
|
107
107
|
expect(record?.takedownId).toBeGreaterThan(0)
|
|
108
108
|
|
|
109
|
-
const recordPds = await network.pds.ctx.
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const recordPds = await network.pds.ctx.actorStore.read(
|
|
110
|
+
post.ref.uri.hostname,
|
|
111
|
+
(store) =>
|
|
112
|
+
store.db.db
|
|
113
|
+
.selectFrom('record')
|
|
114
|
+
.where('uri', '=', post.ref.uriStr)
|
|
115
|
+
.select('takedownRef')
|
|
116
|
+
.executeTakeFirst(),
|
|
117
|
+
)
|
|
114
118
|
expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString())
|
|
115
119
|
|
|
116
120
|
expect(testInvalidator.invalidated.length).toBe(1)
|
|
@@ -162,11 +166,13 @@ describe('takedowner', () => {
|
|
|
162
166
|
.executeTakeFirst()
|
|
163
167
|
expect(record?.takedownId).toBeGreaterThan(0)
|
|
164
168
|
|
|
165
|
-
const recordPds = await network.pds.ctx.
|
|
166
|
-
.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
const recordPds = await network.pds.ctx.actorStore.read(alice, (store) =>
|
|
170
|
+
store.db.db
|
|
171
|
+
.selectFrom('record')
|
|
172
|
+
.where('uri', '=', res.data.uri)
|
|
173
|
+
.select('takedownRef')
|
|
174
|
+
.executeTakeFirst(),
|
|
175
|
+
)
|
|
170
176
|
expect(recordPds?.takedownRef).toEqual(takedownEvent.id.toString())
|
|
171
177
|
|
|
172
178
|
expect(testInvalidator.invalidated.length).toBe(2)
|
|
@@ -77,8 +77,10 @@ describe('blob resolver', () => {
|
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
it('fails on blob with bad signature check.', async () => {
|
|
80
|
-
await network.pds.ctx.blobstore.delete(fileCid)
|
|
81
|
-
await network.pds.ctx
|
|
80
|
+
await network.pds.ctx.blobstore(fileDid).delete(fileCid)
|
|
81
|
+
await network.pds.ctx
|
|
82
|
+
.blobstore(fileDid)
|
|
83
|
+
.putPermanent(fileCid, randomBytes(100))
|
|
82
84
|
const tryGetBlob = client.get(`/blob/${fileDid}/${fileCid.toString()}`)
|
|
83
85
|
await expect(tryGetBlob).rejects.toThrow(
|
|
84
86
|
'maxContentLength size of -1 exceeded',
|