@atproto/bsky 0.0.166 → 0.0.167
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/api/app/bsky/notification/listActivitySubscriptions.d.ts +4 -0
- package/dist/api/app/bsky/notification/listActivitySubscriptions.d.ts.map +1 -0
- package/dist/api/app/bsky/notification/listActivitySubscriptions.js +63 -0
- package/dist/api/app/bsky/notification/listActivitySubscriptions.js.map +1 -0
- package/dist/api/app/bsky/notification/putActivitySubscription.d.ts +4 -0
- package/dist/api/app/bsky/notification/putActivitySubscription.d.ts.map +1 -0
- package/dist/api/app/bsky/notification/putActivitySubscription.js +63 -0
- package/dist/api/app/bsky/notification/putActivitySubscription.js.map +1 -0
- package/dist/api/app/bsky/notification/putPreferencesV2.d.ts.map +1 -1
- package/dist/api/app/bsky/notification/putPreferencesV2.js +2 -1
- package/dist/api/app/bsky/notification/putPreferencesV2.js.map +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +4 -0
- package/dist/api/index.js.map +1 -1
- package/dist/data-plane/bsync/index.d.ts.map +1 -1
- package/dist/data-plane/bsync/index.js +52 -38
- package/dist/data-plane/bsync/index.js.map +1 -1
- package/dist/data-plane/server/db/database-schema.d.ts +2 -1
- package/dist/data-plane/server/db/database-schema.d.ts.map +1 -1
- package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.d.ts +4 -0
- package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.d.ts.map +1 -0
- package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.js +24 -0
- package/dist/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.js.map +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
- package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
- package/dist/data-plane/server/db/migrations/index.js +2 -1
- package/dist/data-plane/server/db/migrations/index.js.map +1 -1
- package/dist/data-plane/server/db/pagination.d.ts +22 -0
- package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
- package/dist/data-plane/server/db/pagination.js +30 -1
- package/dist/data-plane/server/db/pagination.js.map +1 -1
- package/dist/data-plane/server/db/tables/activity-subscription.d.ts +13 -0
- package/dist/data-plane/server/db/tables/activity-subscription.d.ts.map +1 -0
- package/dist/data-plane/server/db/tables/activity-subscription.js +5 -0
- package/dist/data-plane/server/db/tables/activity-subscription.js.map +1 -0
- package/dist/data-plane/server/indexing/index.d.ts +2 -0
- package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/index.js +2 -0
- package/dist/data-plane/server/indexing/index.js.map +1 -1
- package/dist/data-plane/server/indexing/plugins/notif-declaration.d.ts +7 -0
- package/dist/data-plane/server/indexing/plugins/notif-declaration.d.ts.map +1 -0
- package/dist/data-plane/server/indexing/plugins/notif-declaration.js +72 -0
- package/dist/data-plane/server/indexing/plugins/notif-declaration.js.map +1 -0
- package/dist/data-plane/server/routes/activity-subscription.d.ts +6 -0
- package/dist/data-plane/server/routes/activity-subscription.d.ts.map +1 -0
- package/dist/data-plane/server/routes/activity-subscription.js +67 -0
- package/dist/data-plane/server/routes/activity-subscription.js.map +1 -0
- package/dist/data-plane/server/routes/index.js +2 -2
- package/dist/data-plane/server/routes/index.js.map +1 -1
- package/dist/data-plane/server/routes/notifs.d.ts +3 -0
- package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
- package/dist/data-plane/server/routes/notifs.js +64 -0
- package/dist/data-plane/server/routes/notifs.js.map +1 -1
- package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
- package/dist/data-plane/server/routes/profile.js +20 -1
- package/dist/data-plane/server/routes/profile.js.map +1 -1
- package/dist/data-plane/server/routes/records.d.ts.map +1 -1
- package/dist/data-plane/server/routes/records.js +1 -0
- package/dist/data-plane/server/routes/records.js.map +1 -1
- package/dist/data-plane/server/util.d.ts +6 -6
- package/dist/hydration/actor.d.ts +20 -6
- package/dist/hydration/actor.d.ts.map +1 -1
- package/dist/hydration/actor.js +44 -1
- package/dist/hydration/actor.js.map +1 -1
- package/dist/hydration/hydrator.d.ts +4 -3
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +16 -2
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/hydration/util.d.ts +4 -0
- package/dist/hydration/util.d.ts.map +1 -1
- package/dist/hydration/util.js +3 -1
- package/dist/hydration/util.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +4 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +2 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/proto/bsky_connect.d.ts +28 -1
- package/dist/proto/bsky_connect.d.ts.map +1 -1
- package/dist/proto/bsky_connect.js +27 -0
- package/dist/proto/bsky_connect.js.map +1 -1
- package/dist/proto/bsky_pb.d.ts +189 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +598 -5
- package/dist/proto/bsky_pb.js.map +1 -1
- package/dist/stash.d.ts +1 -0
- package/dist/stash.d.ts.map +1 -1
- package/dist/stash.js +1 -0
- package/dist/stash.js.map +1 -1
- package/dist/views/index.d.ts +5 -3
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +29 -9
- package/dist/views/index.js.map +1 -1
- package/package.json +4 -4
- package/proto/bsky.proto +45 -0
- package/src/api/app/bsky/notification/listActivitySubscriptions.ts +110 -0
- package/src/api/app/bsky/notification/putActivitySubscription.ts +69 -0
- package/src/api/app/bsky/notification/putPreferencesV2.ts +2 -1
- package/src/api/index.ts +4 -0
- package/src/data-plane/bsync/index.ts +75 -44
- package/src/data-plane/server/db/database-schema.ts +3 -1
- package/src/data-plane/server/db/migrations/20250611T140649895Z-add-activity-subscription.ts +22 -0
- package/src/data-plane/server/db/migrations/index.ts +1 -0
- package/src/data-plane/server/db/pagination.ts +37 -0
- package/src/data-plane/server/db/tables/activity-subscription.ts +12 -0
- package/src/data-plane/server/indexing/index.ts +3 -0
- package/src/data-plane/server/indexing/plugins/notif-declaration.ts +59 -0
- package/src/data-plane/server/routes/activity-subscription.ts +83 -0
- package/src/data-plane/server/routes/index.ts +2 -2
- package/src/data-plane/server/routes/notifs.ts +95 -0
- package/src/data-plane/server/routes/profile.ts +33 -1
- package/src/data-plane/server/routes/records.ts +4 -0
- package/src/hydration/actor.ts +97 -10
- package/src/hydration/hydrator.ts +32 -6
- package/src/hydration/util.ts +8 -0
- package/src/lexicon/lexicons.ts +4 -0
- package/src/proto/bsky_connect.ts +33 -0
- package/src/proto/bsky_pb.ts +648 -0
- package/src/stash.ts +6 -1
- package/src/views/index.ts +48 -11
- package/tests/__snapshots__/feed-generation.test.ts.snap +213 -0
- package/tests/data-plane/__snapshots__/indexing.test.ts.snap +88 -0
- package/tests/views/__snapshots__/author-feed.test.ts.snap +498 -0
- package/tests/views/__snapshots__/block-lists.test.ts.snap +56 -0
- package/tests/views/__snapshots__/blocks.test.ts.snap +28 -0
- package/tests/views/__snapshots__/follows.test.ts.snap +170 -0
- package/tests/views/__snapshots__/labeler-service.test.ts.snap +15 -0
- package/tests/views/__snapshots__/likes.test.ts.snap +23 -0
- package/tests/views/__snapshots__/list-feed.test.ts.snap +68 -0
- package/tests/views/__snapshots__/lists.test.ts.snap +120 -0
- package/tests/views/__snapshots__/mute-lists.test.ts.snap +63 -0
- package/tests/views/__snapshots__/mutes.test.ts.snap +55 -0
- package/tests/views/__snapshots__/notifications.test.ts.snap +299 -0
- package/tests/views/__snapshots__/posts.test.ts.snap +58 -0
- package/tests/views/__snapshots__/profile.test.ts.snap +74 -0
- package/tests/views/__snapshots__/quotes.test.ts.snap +35 -0
- package/tests/views/__snapshots__/reposts.test.ts.snap +26 -0
- package/tests/views/__snapshots__/starter-packs.test.ts.snap +113 -0
- package/tests/views/__snapshots__/thread-v2.test.ts.snap +115 -0
- package/tests/views/__snapshots__/thread.test.ts.snap +145 -0
- package/tests/views/__snapshots__/timeline.test.ts.snap +566 -0
- package/tests/views/notifications.test.ts +355 -19
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/data-plane/server/routes/private-data.d.ts +0 -9
- package/dist/data-plane/server/routes/private-data.d.ts.map +0 -1
- package/dist/data-plane/server/routes/private-data.js +0 -65
- package/dist/data-plane/server/routes/private-data.js.map +0 -1
- package/src/data-plane/server/routes/private-data.ts +0 -95
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { mapDefined } from '@atproto/common'
|
|
2
|
+
import { AppContext } from '../../../../context'
|
|
3
|
+
import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'
|
|
4
|
+
import { Server } from '../../../../lexicon'
|
|
5
|
+
import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listActivitySubscriptions'
|
|
6
|
+
import {
|
|
7
|
+
HydrationFnInput,
|
|
8
|
+
PresentationFnInput,
|
|
9
|
+
RulesFnInput,
|
|
10
|
+
SkeletonFnInput,
|
|
11
|
+
createPipeline,
|
|
12
|
+
} from '../../../../pipeline'
|
|
13
|
+
import { Views } from '../../../../views'
|
|
14
|
+
import { clearlyBadCursor, resHeaders } from '../../../util'
|
|
15
|
+
|
|
16
|
+
export default function (server: Server, ctx: AppContext) {
|
|
17
|
+
const listActivitySubscriptions = createPipeline(
|
|
18
|
+
skeleton,
|
|
19
|
+
hydration,
|
|
20
|
+
noBlocks,
|
|
21
|
+
presentation,
|
|
22
|
+
)
|
|
23
|
+
server.app.bsky.notification.listActivitySubscriptions({
|
|
24
|
+
auth: ctx.authVerifier.standard,
|
|
25
|
+
handler: async ({ params, auth, req }) => {
|
|
26
|
+
const viewer = auth.credentials.iss
|
|
27
|
+
const labelers = ctx.reqLabelers(req)
|
|
28
|
+
const hydrateCtx = await ctx.hydrator.createContext({
|
|
29
|
+
labelers,
|
|
30
|
+
viewer,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const result = await listActivitySubscriptions(
|
|
34
|
+
{ ...params, hydrateCtx: hydrateCtx.copy({ viewer }) },
|
|
35
|
+
ctx,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
encoding: 'application/json',
|
|
40
|
+
body: result,
|
|
41
|
+
headers: resHeaders({ labelers: hydrateCtx.labelers }),
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
48
|
+
const { params, ctx } = input
|
|
49
|
+
const actorDid = params.hydrateCtx.viewer
|
|
50
|
+
if (clearlyBadCursor(params.cursor)) {
|
|
51
|
+
return { actorDid, dids: [] }
|
|
52
|
+
}
|
|
53
|
+
const { dids, cursor } =
|
|
54
|
+
await ctx.hydrator.dataplane.getActivitySubscriptionDids({
|
|
55
|
+
actorDid: params.hydrateCtx.viewer,
|
|
56
|
+
limit: params.limit,
|
|
57
|
+
cursor: params.cursor,
|
|
58
|
+
})
|
|
59
|
+
return {
|
|
60
|
+
actorDid,
|
|
61
|
+
dids,
|
|
62
|
+
cursor: cursor || undefined,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hydration = async (
|
|
67
|
+
input: HydrationFnInput<Context, Params, SkeletonState>,
|
|
68
|
+
) => {
|
|
69
|
+
const { ctx, params, skeleton } = input
|
|
70
|
+
const { dids } = skeleton
|
|
71
|
+
const state = await ctx.hydrator.hydrateProfilesDetailed(
|
|
72
|
+
dids,
|
|
73
|
+
params.hydrateCtx,
|
|
74
|
+
)
|
|
75
|
+
return state
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const noBlocks = (input: RulesFnInput<Context, Params, SkeletonState>) => {
|
|
79
|
+
const { skeleton, hydration, ctx } = input
|
|
80
|
+
skeleton.dids = skeleton.dids.filter(
|
|
81
|
+
(did) => !ctx.views.viewerBlockExists(did, hydration),
|
|
82
|
+
)
|
|
83
|
+
return skeleton
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const presentation = (
|
|
87
|
+
input: PresentationFnInput<Context, Params, SkeletonState>,
|
|
88
|
+
) => {
|
|
89
|
+
const { ctx, hydration, skeleton } = input
|
|
90
|
+
const { dids, cursor } = skeleton
|
|
91
|
+
const subscriptions = mapDefined(dids, (did) => {
|
|
92
|
+
return ctx.views.profile(did, hydration)
|
|
93
|
+
})
|
|
94
|
+
return { subscriptions, cursor }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type Context = {
|
|
98
|
+
hydrator: Hydrator
|
|
99
|
+
views: Views
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type Params = QueryParams & {
|
|
103
|
+
hydrateCtx: HydrateCtx & { viewer: string }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type SkeletonState = {
|
|
107
|
+
actorDid: string
|
|
108
|
+
dids: string[]
|
|
109
|
+
cursor?: string
|
|
110
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TID } from '@atproto/common'
|
|
2
|
+
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
|
+
import { AppContext } from '../../../../context'
|
|
4
|
+
import { isActivitySubscriptionEnabled } from '../../../../hydration/util'
|
|
5
|
+
import { Server } from '../../../../lexicon'
|
|
6
|
+
import { Namespaces } from '../../../../stash'
|
|
7
|
+
|
|
8
|
+
export default function (server: Server, ctx: AppContext) {
|
|
9
|
+
server.app.bsky.notification.putActivitySubscription({
|
|
10
|
+
auth: ctx.authVerifier.standard,
|
|
11
|
+
handler: async ({ input, auth }) => {
|
|
12
|
+
const actorDid = auth.credentials.iss
|
|
13
|
+
const { subject, activitySubscription } = input.body
|
|
14
|
+
if (actorDid === subject) {
|
|
15
|
+
throw new InvalidRequestError('Cannot subscribe to own activity')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const existingKey = await getExistingKey(ctx, actorDid, subject)
|
|
19
|
+
const enabled = isActivitySubscriptionEnabled(activitySubscription)
|
|
20
|
+
|
|
21
|
+
const stashInput = {
|
|
22
|
+
actorDid,
|
|
23
|
+
namespace:
|
|
24
|
+
Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,
|
|
25
|
+
payload: {
|
|
26
|
+
subject,
|
|
27
|
+
activitySubscription,
|
|
28
|
+
},
|
|
29
|
+
key: existingKey ?? TID.nextStr(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (existingKey) {
|
|
33
|
+
if (enabled) {
|
|
34
|
+
await ctx.stashClient.update(stashInput)
|
|
35
|
+
} else {
|
|
36
|
+
await ctx.stashClient.delete(stashInput)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
if (enabled) {
|
|
40
|
+
await ctx.stashClient.create(stashInput)
|
|
41
|
+
} else {
|
|
42
|
+
// no-op: subscription already doesn't exist
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
encoding: 'application/json',
|
|
48
|
+
body: {
|
|
49
|
+
subject,
|
|
50
|
+
activitySubscription: enabled ? activitySubscription : undefined,
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getExistingKey = async (
|
|
58
|
+
ctx: AppContext,
|
|
59
|
+
actorDid: string,
|
|
60
|
+
subject: string,
|
|
61
|
+
): Promise<string | null> => {
|
|
62
|
+
const res = await ctx.dataplane.getActivitySubscriptionsByActorAndSubjects({
|
|
63
|
+
actorDid,
|
|
64
|
+
subjectDids: [subject],
|
|
65
|
+
})
|
|
66
|
+
const [existing] = res.subscriptions
|
|
67
|
+
const key = existing.key
|
|
68
|
+
return key || null
|
|
69
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Server } from '../../../../lexicon'
|
|
|
6
6
|
import { Preferences } from '../../../../lexicon/types/app/bsky/notification/defs'
|
|
7
7
|
import { HandlerInput } from '../../../../lexicon/types/app/bsky/notification/putPreferencesV2'
|
|
8
8
|
import { GetNotificationPreferencesResponse } from '../../../../proto/bsky_pb'
|
|
9
|
+
import { Namespaces } from '../../../../stash'
|
|
9
10
|
import { protobufToLex } from './util'
|
|
10
11
|
|
|
11
12
|
export default function (server: Server, ctx: AppContext) {
|
|
@@ -18,7 +19,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
18
19
|
// Notification preferences are created automatically on the dataplane on signup, so we just update.
|
|
19
20
|
await ctx.stashClient.update({
|
|
20
21
|
actorDid,
|
|
21
|
-
namespace:
|
|
22
|
+
namespace: Namespaces.AppBskyNotificationDefsPreferences,
|
|
22
23
|
key: 'self',
|
|
23
24
|
payload: preferences,
|
|
24
25
|
})
|
package/src/api/index.ts
CHANGED
|
@@ -44,7 +44,9 @@ import unmuteThread from './app/bsky/graph/unmuteThread'
|
|
|
44
44
|
import getLabelerServices from './app/bsky/labeler/getServices'
|
|
45
45
|
import getPreferences from './app/bsky/notification/getPreferences'
|
|
46
46
|
import getUnreadCount from './app/bsky/notification/getUnreadCount'
|
|
47
|
+
import listActivitySubscriptions from './app/bsky/notification/listActivitySubscriptions'
|
|
47
48
|
import listNotifications from './app/bsky/notification/listNotifications'
|
|
49
|
+
import putActivitySubscription from './app/bsky/notification/putActivitySubscription'
|
|
48
50
|
import putPreferences from './app/bsky/notification/putPreferences'
|
|
49
51
|
import putPreferencesV2 from './app/bsky/notification/putPreferencesV2'
|
|
50
52
|
import registerPush from './app/bsky/notification/registerPush'
|
|
@@ -126,7 +128,9 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
126
128
|
getSuggestions(server, ctx)
|
|
127
129
|
getPreferences(server, ctx)
|
|
128
130
|
getUnreadCount(server, ctx)
|
|
131
|
+
listActivitySubscriptions(server, ctx)
|
|
129
132
|
listNotifications(server, ctx)
|
|
133
|
+
putActivitySubscription(server, ctx)
|
|
130
134
|
updateSeen(server, ctx)
|
|
131
135
|
putPreferences(server, ctx)
|
|
132
136
|
putPreferencesV2(server, ctx)
|
|
@@ -5,8 +5,11 @@ import { ConnectRouter } from '@connectrpc/connect'
|
|
|
5
5
|
import { expressConnectMiddleware } from '@connectrpc/connect-express'
|
|
6
6
|
import express from 'express'
|
|
7
7
|
import { TID } from '@atproto/common'
|
|
8
|
+
import { jsonStringToLex } from '@atproto/lexicon'
|
|
8
9
|
import { AtUri } from '@atproto/syntax'
|
|
9
10
|
import { ids } from '../../lexicon/lexicons'
|
|
11
|
+
import { SubjectActivitySubscription } from '../../lexicon/types/app/bsky/notification/defs'
|
|
12
|
+
import { httpLogger } from '../../logger'
|
|
10
13
|
import { Service } from '../../proto/bsync_connect'
|
|
11
14
|
import {
|
|
12
15
|
Method,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
} from '../../proto/bsync_pb'
|
|
16
19
|
import { Namespaces } from '../../stash'
|
|
17
20
|
import { Database } from '../server/db'
|
|
21
|
+
import { excluded } from '../server/db/util'
|
|
18
22
|
|
|
19
23
|
export class MockBsync {
|
|
20
24
|
constructor(public server: http.Server) {}
|
|
@@ -146,19 +150,30 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
|
|
|
146
150
|
|
|
147
151
|
async putOperation(req) {
|
|
148
152
|
const { actorDid, namespace, key, method, payload } = req
|
|
149
|
-
|
|
150
|
-
method
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
153
|
+
assert(
|
|
154
|
+
method === Method.CREATE ||
|
|
155
|
+
method === Method.UPDATE ||
|
|
156
|
+
method === Method.DELETE,
|
|
157
|
+
`Unsupported method: ${method}`,
|
|
158
|
+
)
|
|
156
159
|
|
|
157
160
|
const now = new Date().toISOString()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
|
|
162
|
+
// index all items into private_data
|
|
163
|
+
await handleGenericOperation(db, req, now)
|
|
164
|
+
|
|
165
|
+
// maintain bespoke indexes for certain namespaces
|
|
166
|
+
if (
|
|
167
|
+
namespace ===
|
|
168
|
+
Namespaces.AppBskyNotificationDefsSubjectActivitySubscription
|
|
169
|
+
) {
|
|
170
|
+
await handleSubjectActivitySubscriptionOperation(db, req, now).catch(
|
|
171
|
+
(err: unknown) =>
|
|
172
|
+
httpLogger.warn(
|
|
173
|
+
{ err, namespace },
|
|
174
|
+
'mock bsync put operation failed',
|
|
175
|
+
),
|
|
176
|
+
)
|
|
162
177
|
}
|
|
163
178
|
|
|
164
179
|
return {
|
|
@@ -182,43 +197,64 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
|
|
|
182
197
|
},
|
|
183
198
|
})
|
|
184
199
|
|
|
185
|
-
const
|
|
200
|
+
const handleSubjectActivitySubscriptionOperation = async (
|
|
186
201
|
db: Database,
|
|
187
202
|
req: PutOperationRequest,
|
|
188
203
|
now: string,
|
|
189
204
|
) => {
|
|
190
|
-
const { actorDid,
|
|
191
|
-
|
|
205
|
+
const { actorDid, key, method, payload } = req
|
|
206
|
+
|
|
207
|
+
if (method === Method.DELETE) {
|
|
192
208
|
return db.db
|
|
193
|
-
.
|
|
209
|
+
.deleteFrom('activity_subscription')
|
|
210
|
+
.where('creator', '=', actorDid)
|
|
211
|
+
.where('key', '=', key)
|
|
212
|
+
.execute()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const parsed = jsonStringToLex(
|
|
216
|
+
Buffer.from(payload).toString('utf8'),
|
|
217
|
+
) as SubjectActivitySubscription
|
|
218
|
+
const {
|
|
219
|
+
subject,
|
|
220
|
+
activitySubscription: { post, reply },
|
|
221
|
+
} = parsed
|
|
222
|
+
|
|
223
|
+
if (method === Method.CREATE) {
|
|
224
|
+
return db.db
|
|
225
|
+
.insertInto('activity_subscription')
|
|
194
226
|
.values({
|
|
195
|
-
actorDid,
|
|
196
|
-
|
|
227
|
+
creator: actorDid,
|
|
228
|
+
subjectDid: subject,
|
|
197
229
|
key,
|
|
198
|
-
payload: Buffer.from(payload).toString('utf8'),
|
|
199
230
|
indexedAt: now,
|
|
200
|
-
|
|
231
|
+
post,
|
|
232
|
+
reply,
|
|
201
233
|
})
|
|
202
|
-
.onConflict((oc) =>
|
|
203
|
-
oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
|
|
204
|
-
payload: Buffer.from(payload).toString('utf8'),
|
|
205
|
-
updatedAt: now,
|
|
206
|
-
}),
|
|
207
|
-
)
|
|
208
234
|
.execute()
|
|
209
235
|
}
|
|
210
236
|
|
|
211
|
-
return
|
|
237
|
+
return db.db
|
|
238
|
+
.updateTable('activity_subscription')
|
|
239
|
+
.where('creator', '=', actorDid)
|
|
240
|
+
.where('key', '=', key)
|
|
241
|
+
.set({
|
|
242
|
+
indexedAt: now,
|
|
243
|
+
post,
|
|
244
|
+
reply,
|
|
245
|
+
})
|
|
246
|
+
.execute()
|
|
212
247
|
}
|
|
213
248
|
|
|
249
|
+
// upsert into or remove from private_data
|
|
214
250
|
const handleGenericOperation = async (
|
|
215
251
|
db: Database,
|
|
216
252
|
req: PutOperationRequest,
|
|
217
253
|
now: string,
|
|
218
254
|
) => {
|
|
219
255
|
const { actorDid, namespace, key, method, payload } = req
|
|
220
|
-
if (method === Method.CREATE) {
|
|
221
|
-
|
|
256
|
+
if (method === Method.CREATE || method === Method.UPDATE) {
|
|
257
|
+
await db.db
|
|
222
258
|
.insertInto('private_data')
|
|
223
259
|
.values({
|
|
224
260
|
actorDid,
|
|
@@ -228,26 +264,21 @@ const handleGenericOperation = async (
|
|
|
228
264
|
indexedAt: now,
|
|
229
265
|
updatedAt: now,
|
|
230
266
|
})
|
|
267
|
+
.onConflict((oc) =>
|
|
268
|
+
oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
|
|
269
|
+
payload: excluded(db.db, 'payload'),
|
|
270
|
+
updatedAt: excluded(db.db, 'updatedAt'),
|
|
271
|
+
}),
|
|
272
|
+
)
|
|
231
273
|
.execute()
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return db.db
|
|
236
|
-
.updateTable('private_data')
|
|
274
|
+
} else if (method === Method.DELETE) {
|
|
275
|
+
await db.db
|
|
276
|
+
.deleteFrom('private_data')
|
|
237
277
|
.where('actorDid', '=', actorDid)
|
|
238
278
|
.where('namespace', '=', namespace)
|
|
239
279
|
.where('key', '=', key)
|
|
240
|
-
.set({
|
|
241
|
-
payload: Buffer.from(payload).toString('utf8'),
|
|
242
|
-
updatedAt: now,
|
|
243
|
-
})
|
|
244
280
|
.execute()
|
|
281
|
+
} else {
|
|
282
|
+
assert.fail(`unexpected method ${method}`)
|
|
245
283
|
}
|
|
246
|
-
|
|
247
|
-
return db.db
|
|
248
|
-
.deleteFrom('private_data')
|
|
249
|
-
.where('actorDid', '=', actorDid)
|
|
250
|
-
.where('namespace', '=', namespace)
|
|
251
|
-
.where('key', '=', key)
|
|
252
|
-
.execute()
|
|
253
284
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Kysely } from 'kysely'
|
|
2
|
+
import * as activitySubscription from './tables/activity-subscription'
|
|
2
3
|
import * as actor from './tables/actor'
|
|
3
4
|
import * as actorBlock from './tables/actor-block'
|
|
4
5
|
import * as actorState from './tables/actor-state'
|
|
@@ -79,6 +80,7 @@ export type DatabaseSchemaType = duplicateRecord.PartialDB &
|
|
|
79
80
|
taggedSuggestion.PartialDB &
|
|
80
81
|
quote.PartialDB &
|
|
81
82
|
verification.PartialDB &
|
|
82
|
-
privateData.PartialDB
|
|
83
|
+
privateData.PartialDB &
|
|
84
|
+
activitySubscription.PartialDB
|
|
83
85
|
|
|
84
86
|
export type DatabaseSchema = Kysely<DatabaseSchemaType>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Kysely } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
+
await db.schema
|
|
5
|
+
.createTable('activity_subscription')
|
|
6
|
+
.addColumn('creator', 'varchar', (col) => col.notNull())
|
|
7
|
+
.addColumn('subjectDid', 'varchar', (col) => col.notNull())
|
|
8
|
+
.addColumn('key', 'varchar', (col) => col.notNull())
|
|
9
|
+
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
|
|
10
|
+
.addColumn('post', 'boolean', (col) => col.notNull())
|
|
11
|
+
.addColumn('reply', 'boolean', (col) => col.notNull())
|
|
12
|
+
.addPrimaryKeyConstraint('activity_subscription_pkey', ['creator', 'key'])
|
|
13
|
+
.addUniqueConstraint('activity_subscription_unique_creator_subject_did', [
|
|
14
|
+
'creator',
|
|
15
|
+
'subjectDid',
|
|
16
|
+
])
|
|
17
|
+
.execute()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
21
|
+
await db.schema.dropTable('activity_subscription').execute()
|
|
22
|
+
}
|
|
@@ -51,3 +51,4 @@ export * as _20250404T163421487Z from './20250404T163421487Z-verifications'
|
|
|
51
51
|
export * as _20250526T023712742Z from './20250526T023712742Z-like-repost-via'
|
|
52
52
|
export * as _20250528T221913281Z from './20250528T221913281Z-add-record-tags'
|
|
53
53
|
export * as _20250602T190357447Z from './20250602T190357447Z-add-private-data'
|
|
54
|
+
export * as _20250611T140649895Z from './20250611T140649895Z-add-activity-subscription'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sql } from 'kysely'
|
|
2
|
+
import { ensureValidRecordKey } from '@atproto/syntax'
|
|
2
3
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
3
4
|
import { AnyQb, DbRef } from './util'
|
|
4
5
|
|
|
@@ -294,3 +295,39 @@ export class IsoSortAtKey extends IsoTimeKey<{
|
|
|
294
295
|
return { primary: result.sortAt }
|
|
295
296
|
}
|
|
296
297
|
}
|
|
298
|
+
|
|
299
|
+
type KeyResult = { key: string }
|
|
300
|
+
type RkeyLabeledResult = SingleKeyCursor
|
|
301
|
+
|
|
302
|
+
export class RkeyKey<RkeyResult = KeyResult> extends GenericSingleKey<
|
|
303
|
+
RkeyResult,
|
|
304
|
+
RkeyLabeledResult
|
|
305
|
+
> {
|
|
306
|
+
labelResult(result: RkeyResult): RkeyLabeledResult
|
|
307
|
+
labelResult<RkeyResult extends KeyResult>(result: RkeyResult) {
|
|
308
|
+
return { primary: result }
|
|
309
|
+
}
|
|
310
|
+
labeledResultToCursor(labeled: RkeyLabeledResult) {
|
|
311
|
+
return {
|
|
312
|
+
primary: labeled.primary,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
cursorToLabeledResult(cursor: SingleKeyCursor) {
|
|
316
|
+
try {
|
|
317
|
+
ensureValidRecordKey(cursor.primary)
|
|
318
|
+
return {
|
|
319
|
+
primary: cursor.primary,
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
throw new InvalidRequestError('Malformed cursor')
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export class StashKeyKey extends RkeyKey<{
|
|
328
|
+
key: string
|
|
329
|
+
}> {
|
|
330
|
+
labelResult(result: { key: string }) {
|
|
331
|
+
return { primary: result.key }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const tableName = 'activity_subscription'
|
|
2
|
+
export interface ActivitySubscription {
|
|
3
|
+
creator: string
|
|
4
|
+
subjectDid: string
|
|
5
|
+
// key from the bsync stash.
|
|
6
|
+
key: string
|
|
7
|
+
indexedAt: string
|
|
8
|
+
post: boolean
|
|
9
|
+
reply: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PartialDB = { [tableName]: ActivitySubscription }
|
|
@@ -26,6 +26,7 @@ import * as Like from './plugins/like'
|
|
|
26
26
|
import * as List from './plugins/list'
|
|
27
27
|
import * as ListBlock from './plugins/list-block'
|
|
28
28
|
import * as ListItem from './plugins/list-item'
|
|
29
|
+
import * as NotifDeclaration from './plugins/notif-declaration'
|
|
29
30
|
import * as Post from './plugins/post'
|
|
30
31
|
import * as Postgate from './plugins/post-gate'
|
|
31
32
|
import * as Profile from './plugins/profile'
|
|
@@ -52,6 +53,7 @@ export class IndexingService {
|
|
|
52
53
|
feedGenerator: FeedGenerator.PluginType
|
|
53
54
|
starterPack: StarterPack.PluginType
|
|
54
55
|
labeler: Labeler.PluginType
|
|
56
|
+
notifDeclaration: NotifDeclaration.PluginType
|
|
55
57
|
chatDeclaration: ChatDeclaration.PluginType
|
|
56
58
|
verification: Verification.PluginType
|
|
57
59
|
status: Status.PluginType
|
|
@@ -77,6 +79,7 @@ export class IndexingService {
|
|
|
77
79
|
feedGenerator: FeedGenerator.makePlugin(this.db, this.background),
|
|
78
80
|
starterPack: StarterPack.makePlugin(this.db, this.background),
|
|
79
81
|
labeler: Labeler.makePlugin(this.db, this.background),
|
|
82
|
+
notifDeclaration: NotifDeclaration.makePlugin(this.db, this.background),
|
|
80
83
|
chatDeclaration: ChatDeclaration.makePlugin(this.db, this.background),
|
|
81
84
|
verification: Verification.makePlugin(this.db, this.background),
|
|
82
85
|
status: Status.makePlugin(this.db, this.background),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid'
|
|
2
|
+
import { AtUri } from '@atproto/syntax'
|
|
3
|
+
import * as lex from '../../../../lexicon/lexicons'
|
|
4
|
+
import { BackgroundQueue } from '../../background'
|
|
5
|
+
import { Database } from '../../db'
|
|
6
|
+
import { DatabaseSchema } from '../../db/database-schema'
|
|
7
|
+
import { RecordProcessor } from '../processor'
|
|
8
|
+
|
|
9
|
+
// @NOTE this indexer is a placeholder to ensure it gets indexed in the generic records table
|
|
10
|
+
const lexId = lex.ids.AppBskyNotificationDeclaration
|
|
11
|
+
|
|
12
|
+
const insertFn = async (
|
|
13
|
+
_db: DatabaseSchema,
|
|
14
|
+
uri: AtUri,
|
|
15
|
+
_cid: CID,
|
|
16
|
+
_obj: unknown,
|
|
17
|
+
_timestamp: string,
|
|
18
|
+
): Promise<unknown | null> => {
|
|
19
|
+
if (uri.rkey !== 'self') return null
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const findDuplicate = async (): Promise<AtUri | null> => {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const notifsForInsert = () => {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const deleteFn = async (
|
|
32
|
+
_db: DatabaseSchema,
|
|
33
|
+
uri: AtUri,
|
|
34
|
+
): Promise<unknown | null> => {
|
|
35
|
+
if (uri.rkey !== 'self') return null
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const notifsForDelete = () => {
|
|
40
|
+
return { notifs: [], toDelete: [] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PluginType = RecordProcessor<unknown, unknown>
|
|
44
|
+
|
|
45
|
+
export const makePlugin = (
|
|
46
|
+
db: Database,
|
|
47
|
+
background: BackgroundQueue,
|
|
48
|
+
): PluginType => {
|
|
49
|
+
return new RecordProcessor(db, background, {
|
|
50
|
+
lexId,
|
|
51
|
+
insertFn,
|
|
52
|
+
findDuplicate,
|
|
53
|
+
deleteFn,
|
|
54
|
+
notifsForInsert,
|
|
55
|
+
notifsForDelete,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default makePlugin
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { PlainMessage } from '@bufbuild/protobuf'
|
|
2
|
+
import { ServiceImpl } from '@connectrpc/connect'
|
|
3
|
+
import { keyBy } from '@atproto/common'
|
|
4
|
+
import { Service } from '../../../proto/bsky_connect'
|
|
5
|
+
import {
|
|
6
|
+
ActivitySubscription,
|
|
7
|
+
GetActivitySubscriptionsByActorAndSubjectsResponse,
|
|
8
|
+
} from '../../../proto/bsky_pb'
|
|
9
|
+
import { Namespaces } from '../../../stash'
|
|
10
|
+
import { Database } from '../db'
|
|
11
|
+
import { StashKeyKey } from '../db/pagination'
|
|
12
|
+
|
|
13
|
+
export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
|
|
14
|
+
async getActivitySubscriptionsByActorAndSubjects(req) {
|
|
15
|
+
const { actorDid, subjectDids } = req
|
|
16
|
+
if (subjectDids.length === 0) {
|
|
17
|
+
return new GetActivitySubscriptionsByActorAndSubjectsResponse({
|
|
18
|
+
subscriptions: [],
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const res = await db.db
|
|
23
|
+
.selectFrom('activity_subscription')
|
|
24
|
+
.selectAll()
|
|
25
|
+
.where('creator', '=', actorDid)
|
|
26
|
+
.where('subjectDid', 'in', subjectDids)
|
|
27
|
+
.execute()
|
|
28
|
+
|
|
29
|
+
const bySubject = keyBy(res, 'subjectDid')
|
|
30
|
+
const subscriptions = subjectDids.map(
|
|
31
|
+
(did): PlainMessage<ActivitySubscription> => {
|
|
32
|
+
const subject = bySubject.get(did)
|
|
33
|
+
if (!subject) {
|
|
34
|
+
return {
|
|
35
|
+
actorDid,
|
|
36
|
+
namespace:
|
|
37
|
+
Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,
|
|
38
|
+
key: '',
|
|
39
|
+
post: undefined,
|
|
40
|
+
reply: undefined,
|
|
41
|
+
subjectDid: '',
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
actorDid,
|
|
47
|
+
namespace:
|
|
48
|
+
Namespaces.AppBskyNotificationDefsSubjectActivitySubscription,
|
|
49
|
+
key: subject.key,
|
|
50
|
+
post: subject.post ? {} : undefined,
|
|
51
|
+
reply: subject.reply ? {} : undefined,
|
|
52
|
+
subjectDid: subject.subjectDid,
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
subscriptions,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async getActivitySubscriptionDids(req) {
|
|
63
|
+
const { actorDid, cursor, limit } = req
|
|
64
|
+
|
|
65
|
+
let builder = db.db
|
|
66
|
+
.selectFrom('activity_subscription')
|
|
67
|
+
.selectAll()
|
|
68
|
+
.where('creator', '=', actorDid)
|
|
69
|
+
|
|
70
|
+
const { ref } = db.db.dynamic
|
|
71
|
+
const key = new StashKeyKey(ref('activity_subscription.key'))
|
|
72
|
+
builder = key.paginate(builder, {
|
|
73
|
+
cursor,
|
|
74
|
+
limit,
|
|
75
|
+
})
|
|
76
|
+
const res = await builder.execute()
|
|
77
|
+
const dids = res.map(({ subjectDid }) => subjectDid)
|
|
78
|
+
return {
|
|
79
|
+
dids,
|
|
80
|
+
cursor: key.packFromResult(res),
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|