@atproto/bsky 0.0.238 → 0.0.240
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 +20 -0
- package/dist/api/app/bsky/actor/searchActors.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/searchActors.js +26 -1
- package/dist/api/app/bsky/actor/searchActors.js.map +1 -1
- package/dist/api/app/bsky/actor/searchActorsTypeahead.d.ts.map +1 -1
- package/dist/api/app/bsky/actor/searchActorsTypeahead.js +26 -2
- package/dist/api/app/bsky/actor/searchActorsTypeahead.js.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/getFeed.js +1 -0
- package/dist/api/app/bsky/feed/getFeed.js.map +1 -1
- package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/searchPosts.js +56 -1
- package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
- package/dist/api/app/bsky/graph/searchStarterPacks.d.ts.map +1 -1
- package/dist/api/app/bsky/graph/searchStarterPacks.js +26 -1
- package/dist/api/app/bsky/graph/searchStarterPacks.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js +27 -6
- package/dist/api/app/bsky/unspecced/getPopularFeedGenerators.js.map +1 -1
- package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.d.ts +4 -0
- package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.d.ts.map +1 -0
- package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.js +17 -0
- package/dist/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.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 +1 -0
- package/dist/data-plane/server/db/migrations/index.js.map +1 -1
- package/dist/data-plane/server/db/tables/post-embed.d.ts +8 -0
- package/dist/data-plane/server/db/tables/post-embed.d.ts.map +1 -1
- package/dist/data-plane/server/db/tables/post-embed.js +1 -0
- package/dist/data-plane/server/db/tables/post-embed.js.map +1 -1
- package/dist/data-plane/server/indexing/index.d.ts +1 -1
- package/dist/data-plane/server/indexing/index.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/index.js +8 -0
- package/dist/data-plane/server/indexing/index.js.map +1 -1
- package/dist/data-plane/server/indexing/plugins/post.d.ts +2 -1
- package/dist/data-plane/server/indexing/plugins/post.d.ts.map +1 -1
- package/dist/data-plane/server/indexing/plugins/post.js +39 -1
- package/dist/data-plane/server/indexing/plugins/post.js.map +1 -1
- package/dist/data-plane/server/routes/feed-gens.d.ts.map +1 -1
- package/dist/data-plane/server/routes/feed-gens.js +21 -12
- package/dist/data-plane/server/routes/feed-gens.js.map +1 -1
- package/dist/data-plane/server/routes/feeds.d.ts.map +1 -1
- package/dist/data-plane/server/routes/feeds.js +7 -2
- package/dist/data-plane/server/routes/feeds.js.map +1 -1
- package/dist/data-plane/server/routes/search.d.ts.map +1 -1
- package/dist/data-plane/server/routes/search.js +62 -12
- package/dist/data-plane/server/routes/search.js.map +1 -1
- package/dist/feature-gates/gates.d.ts +1 -0
- package/dist/feature-gates/gates.d.ts.map +1 -1
- package/dist/feature-gates/gates.js +1 -0
- package/dist/feature-gates/gates.js.map +1 -1
- package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts +4 -0
- package/dist/lexicons/chat/bsky/convo/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/convo/defs.defs.js +1 -0
- package/dist/lexicons/chat/bsky/convo/defs.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js +1 -0
- package/dist/lexicons/chat/bsky/convo/unlockConvo.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.js +5 -1
- package/dist/lexicons/chat/bsky/embed/joinLink.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/createGroup.defs.d.ts +4 -4
- package/dist/lexicons/chat/bsky/group/createGroup.defs.js +2 -2
- package/dist/lexicons/chat/bsky/group/createGroup.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/defs.defs.d.ts +26 -2
- package/dist/lexicons/chat/bsky/group/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group/defs.defs.js +15 -2
- package/dist/lexicons/chat/bsky/group/defs.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts +3 -3
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js +6 -2
- package/dist/lexicons/chat/bsky/group/getJoinLinkPreviews.defs.js.map +1 -1
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts +1 -1
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.d.ts.map +1 -1
- package/dist/lexicons/chat/bsky/moderation/subscribeModEvents.defs.js.map +1 -1
- package/dist/proto/bsky_connect.d.ts +49 -2
- package/dist/proto/bsky_connect.d.ts.map +1 -1
- package/dist/proto/bsky_connect.js +49 -2
- package/dist/proto/bsky_connect.js.map +1 -1
- package/dist/proto/bsky_pb.d.ts +482 -0
- package/dist/proto/bsky_pb.d.ts.map +1 -1
- package/dist/proto/bsky_pb.js +608 -0
- package/dist/proto/bsky_pb.js.map +1 -1
- package/package.json +2 -2
- package/proto/bsky.proto +166 -2
- package/src/api/app/bsky/actor/searchActors.ts +35 -1
- package/src/api/app/bsky/actor/searchActorsTypeahead.ts +35 -2
- package/src/api/app/bsky/feed/getFeed.ts +1 -0
- package/src/api/app/bsky/feed/searchPosts.ts +60 -1
- package/src/api/app/bsky/graph/searchStarterPacks.ts +35 -1
- package/src/api/app/bsky/unspecced/getPopularFeedGenerators.ts +28 -6
- package/src/data-plane/server/db/migrations/20260604T224952774Z-post-embed-gallery-image.ts +19 -0
- package/src/data-plane/server/db/migrations/index.ts +1 -0
- package/src/data-plane/server/db/tables/post-embed.ts +9 -0
- package/src/data-plane/server/indexing/index.ts +8 -0
- package/src/data-plane/server/indexing/plugins/post.ts +49 -1
- package/src/data-plane/server/routes/feed-gens.ts +33 -14
- package/src/data-plane/server/routes/feeds.ts +17 -4
- package/src/data-plane/server/routes/search.ts +81 -13
- package/src/feature-gates/gates.ts +1 -0
- package/tests/data-plane/handle-invalidation.test.ts +2 -1
- package/tests/views/author-feed.test.ts +47 -0
- package/tsconfig.build.json +2 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.tests.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/bsky",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.240",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Reference implementation of app.bsky App View (Bluesky API)",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"zod": "3.23.8",
|
|
51
51
|
"@atproto-labs/fetch-node": "^0.3.0",
|
|
52
52
|
"@atproto-labs/xrpc-utils": "^0.1.0",
|
|
53
|
-
"@atproto/api": "^0.20.
|
|
53
|
+
"@atproto/api": "^0.20.12",
|
|
54
54
|
"@atproto/common": "^0.6.1",
|
|
55
55
|
"@atproto/crypto": "^0.5.0",
|
|
56
56
|
"@atproto/did": "^0.5.0",
|
package/proto/bsky.proto
CHANGED
|
@@ -1086,7 +1086,7 @@ message GetThreadResponse {
|
|
|
1086
1086
|
}
|
|
1087
1087
|
|
|
1088
1088
|
//
|
|
1089
|
-
// Search
|
|
1089
|
+
// Search V1
|
|
1090
1090
|
//
|
|
1091
1091
|
|
|
1092
1092
|
// - Return DIDs of actors matching term, paginated
|
|
@@ -1128,6 +1128,163 @@ message SearchStarterPacksResponse {
|
|
|
1128
1128
|
string cursor = 2;
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
+
//
|
|
1132
|
+
// Search V2
|
|
1133
|
+
//
|
|
1134
|
+
|
|
1135
|
+
enum SearchSortOrder {
|
|
1136
|
+
SEARCH_SORT_ORDER_UNSPECIFIED = 0;
|
|
1137
|
+
SEARCH_SORT_ORDER_RECENT = 1;
|
|
1138
|
+
SEARCH_SORT_ORDER_TOP = 2;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
enum SearchQueryLanguage {
|
|
1142
|
+
SEARCH_QUERY_LANGUAGE_UNSPECIFIED = 0;
|
|
1143
|
+
SEARCH_QUERY_LANGUAGE_JA = 1; // Japanese (Kuromoji)
|
|
1144
|
+
SEARCH_QUERY_LANGUAGE_ZH = 2; // Chinese (smartcn)
|
|
1145
|
+
SEARCH_QUERY_LANGUAGE_KO = 3; // Korean (nori)
|
|
1146
|
+
SEARCH_QUERY_LANGUAGE_TH = 4; // Thai
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Shared request params
|
|
1150
|
+
|
|
1151
|
+
message SearchParams {
|
|
1152
|
+
string query = 1;
|
|
1153
|
+
string viewer = 2;
|
|
1154
|
+
|
|
1155
|
+
// Pagination
|
|
1156
|
+
int32 limit = 3;
|
|
1157
|
+
string cursor = 4;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Shared result types
|
|
1161
|
+
|
|
1162
|
+
// SearchRecordResult represents a generic result hit corresponding to a record.
|
|
1163
|
+
message SearchRecordResult {
|
|
1164
|
+
// AT URI of the record, e.g. "at://did:example:alice/app.bsky.feed.post/12345"
|
|
1165
|
+
string uri = 1;
|
|
1166
|
+
double score = 2;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
message SearchActorResult {
|
|
1170
|
+
string did = 1;
|
|
1171
|
+
double score = 2;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
message PageInfo {
|
|
1175
|
+
string cursor = 1;
|
|
1176
|
+
int64 hits_total = 2;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Search queries
|
|
1180
|
+
|
|
1181
|
+
// PostFilters are logical filters that should be used to match or exclude posts.
|
|
1182
|
+
// Each repeated field is applied as OR within the field with all fields applied together as AND.
|
|
1183
|
+
// example:
|
|
1184
|
+
// filters: {
|
|
1185
|
+
// authors: [alice, bob]
|
|
1186
|
+
// mentions: [carol]
|
|
1187
|
+
// }
|
|
1188
|
+
// => (author is alice OR bob) AND (mentions carol)
|
|
1189
|
+
//
|
|
1190
|
+
message PostsFilters {
|
|
1191
|
+
// Author/mention filters
|
|
1192
|
+
repeated string authors = 1;
|
|
1193
|
+
repeated string mentions = 2;
|
|
1194
|
+
|
|
1195
|
+
// Content filters
|
|
1196
|
+
repeated string domains = 3;
|
|
1197
|
+
repeated string urls = 4;
|
|
1198
|
+
repeated string embed_uris = 5;
|
|
1199
|
+
repeated string hashtags = 6;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
message SearchPostsV2Request {
|
|
1203
|
+
// Required fields
|
|
1204
|
+
SearchParams params = 1;
|
|
1205
|
+
SearchSortOrder sort = 2;
|
|
1206
|
+
|
|
1207
|
+
// Logical filters and exclude fields applied together as AND
|
|
1208
|
+
//
|
|
1209
|
+
// filters: include posts matching the filters
|
|
1210
|
+
PostsFilters filters = 3;
|
|
1211
|
+
|
|
1212
|
+
// Exclude filters: exclude posts matching the filters
|
|
1213
|
+
PostsFilters exclude = 4;
|
|
1214
|
+
|
|
1215
|
+
// Date range filters
|
|
1216
|
+
optional google.protobuf.Timestamp since = 5;
|
|
1217
|
+
optional google.protobuf.Timestamp until = 6; // defaults to "now"
|
|
1218
|
+
// If false, will only query against last 30 days of posts
|
|
1219
|
+
// so `since` and `until` will silently be capped
|
|
1220
|
+
optional bool all_time = 7;
|
|
1221
|
+
|
|
1222
|
+
// Language filter - match posts in this language
|
|
1223
|
+
// Supports 2-char ISO prefixes ("en", "ja, "fr", etc")
|
|
1224
|
+
// and compares against the 2-char prefix of any `langs` field values of the post record
|
|
1225
|
+
// e.g. a post with langs=["en-US", "fr"] would match "en" or "fr" but not "ja"
|
|
1226
|
+
optional string language = 8;
|
|
1227
|
+
|
|
1228
|
+
// Media filters
|
|
1229
|
+
optional bool has_media = 9;
|
|
1230
|
+
optional bool has_video = 10;
|
|
1231
|
+
|
|
1232
|
+
// Reply filters
|
|
1233
|
+
optional string reply_parent_uri = 11;
|
|
1234
|
+
optional string thread_root_uri = 12;
|
|
1235
|
+
optional bool exclude_replies = 13;
|
|
1236
|
+
optional bool replies_only = 14;
|
|
1237
|
+
|
|
1238
|
+
// Social filters
|
|
1239
|
+
optional bool following = 15;
|
|
1240
|
+
|
|
1241
|
+
// Query analysis hint — forces a specific language analyzer.
|
|
1242
|
+
// Auto-detects from query text when unset.
|
|
1243
|
+
optional SearchQueryLanguage query_language = 16;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
message SearchPostsV2Response {
|
|
1247
|
+
repeated SearchRecordResult posts = 1;
|
|
1248
|
+
PageInfo page_info = 2;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
message SearchActorsV2Request {
|
|
1252
|
+
SearchParams params = 1;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
message SearchActorsV2Response {
|
|
1256
|
+
repeated SearchActorResult actors = 1;
|
|
1257
|
+
PageInfo page_info = 2;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
message SearchActorsTypeaheadRequest {
|
|
1261
|
+
string viewer = 1;
|
|
1262
|
+
string query = 2;
|
|
1263
|
+
int32 limit = 3;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
message SearchActorsTypeaheadResponse {
|
|
1267
|
+
repeated SearchActorResult actors = 1;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
message SearchFeedGeneratorsV2Request {
|
|
1271
|
+
SearchParams params = 1;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
message SearchFeedGeneratorsV2Response {
|
|
1275
|
+
repeated SearchRecordResult feed_generators = 1;
|
|
1276
|
+
PageInfo page_info = 2;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
message SearchStarterPacksV2Request {
|
|
1280
|
+
SearchParams params = 1;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
message SearchStarterPacksV2Response {
|
|
1284
|
+
repeated SearchRecordResult starter_packs = 1;
|
|
1285
|
+
PageInfo page_info = 2;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1131
1288
|
//
|
|
1132
1289
|
// Suggestions
|
|
1133
1290
|
//
|
|
@@ -1532,11 +1689,18 @@ service Service {
|
|
|
1532
1689
|
// Threads
|
|
1533
1690
|
rpc GetThread(GetThreadRequest) returns (GetThreadResponse);
|
|
1534
1691
|
|
|
1535
|
-
// Search
|
|
1692
|
+
// Search (V1)
|
|
1536
1693
|
rpc SearchActors(SearchActorsRequest) returns (SearchActorsResponse);
|
|
1537
1694
|
rpc SearchPosts(SearchPostsRequest) returns (SearchPostsResponse);
|
|
1538
1695
|
rpc SearchStarterPacks(SearchStarterPacksRequest) returns (SearchStarterPacksResponse);
|
|
1539
1696
|
|
|
1697
|
+
// Search V2
|
|
1698
|
+
rpc SearchPostsV2(SearchPostsV2Request) returns (SearchPostsV2Response);
|
|
1699
|
+
rpc SearchActorsV2(SearchActorsV2Request) returns (SearchActorsV2Response);
|
|
1700
|
+
rpc SearchActorsTypeahead(SearchActorsTypeaheadRequest) returns (SearchActorsTypeaheadResponse);
|
|
1701
|
+
rpc SearchFeedGeneratorsV2(SearchFeedGeneratorsV2Request) returns (SearchFeedGeneratorsV2Response);
|
|
1702
|
+
rpc SearchStarterPacksV2(SearchStarterPacksV2Request) returns (SearchStarterPacksV2Response);
|
|
1703
|
+
|
|
1540
1704
|
// Suggestions
|
|
1541
1705
|
rpc GetFollowSuggestions(GetFollowSuggestionsRequest) returns (GetFollowSuggestionsResponse);
|
|
1542
1706
|
rpc GetSuggestedEntities(GetSuggestedEntitiesRequest) returns (GetSuggestedEntitiesResponse);
|
|
@@ -34,6 +34,12 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
34
34
|
labelers,
|
|
35
35
|
includeTakedowns,
|
|
36
36
|
skipViewerBlocks,
|
|
37
|
+
features: ctx.featureGatesClient.scope(
|
|
38
|
+
ctx.featureGatesClient.parseUserContextFromHandler({
|
|
39
|
+
viewer,
|
|
40
|
+
req,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
37
43
|
})
|
|
38
44
|
const results = await searchActors({ ...params, hydrateCtx }, ctx)
|
|
39
45
|
return {
|
|
@@ -45,7 +51,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
45
51
|
})
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
const
|
|
54
|
+
const skeletonV1 = async (
|
|
49
55
|
inputs: SkeletonFnInput<Context, Params>,
|
|
50
56
|
): Promise<Skeleton> => {
|
|
51
57
|
const { ctx, params } = inputs
|
|
@@ -82,6 +88,34 @@ const skeleton = async (
|
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
const skeletonV2 = async (
|
|
92
|
+
inputs: SkeletonFnInput<Context, Params>,
|
|
93
|
+
): Promise<Skeleton> => {
|
|
94
|
+
const { ctx, params } = inputs
|
|
95
|
+
const term = params.q ?? params.term ?? ''
|
|
96
|
+
|
|
97
|
+
const res = await ctx.dataplane.searchActorsV2({
|
|
98
|
+
params: {
|
|
99
|
+
query: term,
|
|
100
|
+
viewer: params.hydrateCtx.viewer ?? undefined,
|
|
101
|
+
limit: params.limit,
|
|
102
|
+
cursor: params.cursor,
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
return {
|
|
106
|
+
dids: res.actors.map(({ did }) => did as DidString),
|
|
107
|
+
cursor: parseString(res.pageInfo?.cursor),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
112
|
+
const useV2 = input.params.hydrateCtx.features.checkGate(
|
|
113
|
+
input.params.hydrateCtx.features.Gate.SearchV2Enable,
|
|
114
|
+
)
|
|
115
|
+
const skeletonFn = useV2 ? skeletonV2 : skeletonV1
|
|
116
|
+
return skeletonFn(input)
|
|
117
|
+
}
|
|
118
|
+
|
|
85
119
|
const hydration = async (
|
|
86
120
|
inputs: HydrationFnInput<Context, Params, Skeleton>,
|
|
87
121
|
) => {
|
|
@@ -27,7 +27,16 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
27
27
|
handler: async ({ params, auth, req }) => {
|
|
28
28
|
const viewer = auth.credentials.iss
|
|
29
29
|
const labelers = ctx.reqLabelers(req)
|
|
30
|
-
const hydrateCtx = await ctx.hydrator.createContext({
|
|
30
|
+
const hydrateCtx = await ctx.hydrator.createContext({
|
|
31
|
+
labelers,
|
|
32
|
+
viewer,
|
|
33
|
+
features: ctx.featureGatesClient.scope(
|
|
34
|
+
ctx.featureGatesClient.parseUserContextFromHandler({
|
|
35
|
+
viewer,
|
|
36
|
+
req,
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
})
|
|
31
40
|
const results = await searchActorsTypeahead(
|
|
32
41
|
{ ...params, hydrateCtx },
|
|
33
42
|
ctx,
|
|
@@ -41,7 +50,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
41
50
|
})
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
const
|
|
53
|
+
const skeletonV1 = async (
|
|
45
54
|
inputs: SkeletonFnInput<Context, Params>,
|
|
46
55
|
): Promise<Skeleton> => {
|
|
47
56
|
const { ctx, params } = inputs
|
|
@@ -75,6 +84,30 @@ const skeleton = async (
|
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
const skeletonV2 = async (
|
|
88
|
+
inputs: SkeletonFnInput<Context, Params>,
|
|
89
|
+
): Promise<Skeleton> => {
|
|
90
|
+
const { ctx, params } = inputs
|
|
91
|
+
const term = params.q ?? params.term ?? ''
|
|
92
|
+
|
|
93
|
+
const res = await ctx.dataplane.searchActorsTypeahead({
|
|
94
|
+
viewer: params.hydrateCtx.viewer ?? undefined,
|
|
95
|
+
query: term,
|
|
96
|
+
limit: params.limit,
|
|
97
|
+
})
|
|
98
|
+
return {
|
|
99
|
+
dids: res.actors.map(({ did }) => did as DidString),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
104
|
+
const useV2 = input.params.hydrateCtx.features.checkGate(
|
|
105
|
+
input.params.hydrateCtx.features.Gate.SearchV2Enable,
|
|
106
|
+
)
|
|
107
|
+
const skeletonFn = useV2 ? skeletonV2 : skeletonV1
|
|
108
|
+
return skeletonFn(input)
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
const hydration = async (
|
|
79
112
|
inputs: HydrationFnInput<Context, Params, Skeleton>,
|
|
80
113
|
) => {
|
|
@@ -210,6 +210,7 @@ const skeletonFromFeedGen = async (
|
|
|
210
210
|
// @TODO currently passthrough auth headers from pds
|
|
211
211
|
const result = await xrpcSafe(fgEndpoint, app.bsky.feed.getFeedSkeleton, {
|
|
212
212
|
strictResponseProcessing: false,
|
|
213
|
+
signal: AbortSignal.timeout(10_000),
|
|
213
214
|
headers,
|
|
214
215
|
params: {
|
|
215
216
|
feed: params.feed,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Timestamp } from '@bufbuild/protobuf'
|
|
1
2
|
import { mapDefined } from '@atproto/common'
|
|
2
3
|
import { AtUriString, Client } from '@atproto/lex'
|
|
3
4
|
import { Server } from '@atproto/xrpc-server'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
SkeletonFnInput,
|
|
19
20
|
createPipeline,
|
|
20
21
|
} from '../../../../pipeline.js'
|
|
22
|
+
import { SearchSortOrder } from '../../../../proto/bsky_pb.js'
|
|
21
23
|
import { uriToDid as creatorFromUri } from '../../../../util/uris.js'
|
|
22
24
|
import { Views } from '../../../../views/index.js'
|
|
23
25
|
import { resHeaders } from '../../../util.js'
|
|
@@ -60,7 +62,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
60
62
|
})
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
const
|
|
65
|
+
const skeletonV1 = async (
|
|
64
66
|
inputs: SkeletonFnInput<Context, Params>,
|
|
65
67
|
): Promise<Skeleton> => {
|
|
66
68
|
const { ctx, params } = inputs
|
|
@@ -107,6 +109,50 @@ const skeleton = async (
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
const skeletonV2 = async (
|
|
113
|
+
inputs: SkeletonFnInput<Context, Params>,
|
|
114
|
+
): Promise<Skeleton> => {
|
|
115
|
+
const { ctx, params } = inputs
|
|
116
|
+
const parsedQuery = parsePostSearchQuery(params.q, {
|
|
117
|
+
author: params.author,
|
|
118
|
+
})
|
|
119
|
+
const res = await ctx.dataplane.searchPostsV2({
|
|
120
|
+
params: {
|
|
121
|
+
query: params.q,
|
|
122
|
+
viewer: params.hydrateCtx.viewer ?? undefined,
|
|
123
|
+
limit: params.limit,
|
|
124
|
+
cursor: params.cursor,
|
|
125
|
+
},
|
|
126
|
+
sort: postSortToV2(params.sort),
|
|
127
|
+
filters: {
|
|
128
|
+
authors: params.author ? [params.author] : [],
|
|
129
|
+
mentions: params.mentions ? [params.mentions] : [],
|
|
130
|
+
domains: params.domain ? [params.domain] : [],
|
|
131
|
+
urls: params.url ? [params.url] : [],
|
|
132
|
+
hashtags: params.tag ?? [],
|
|
133
|
+
},
|
|
134
|
+
since: parseTimestamp(params.since),
|
|
135
|
+
until: parseTimestamp(params.until),
|
|
136
|
+
language: params.lang,
|
|
137
|
+
})
|
|
138
|
+
return {
|
|
139
|
+
posts: res.posts.map(({ uri }) => uri as AtUriString),
|
|
140
|
+
cursor: parseString(res.pageInfo?.cursor),
|
|
141
|
+
hitsTotal: res.pageInfo?.hitsTotal
|
|
142
|
+
? Number(res.pageInfo.hitsTotal)
|
|
143
|
+
: undefined,
|
|
144
|
+
parsedQuery,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
149
|
+
const useV2 = input.params.hydrateCtx.features.checkGate(
|
|
150
|
+
input.params.hydrateCtx.features.Gate.SearchV2Enable,
|
|
151
|
+
)
|
|
152
|
+
const skeletonFn = useV2 ? skeletonV2 : skeletonV1
|
|
153
|
+
return skeletonFn(input)
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
const hydration = async (
|
|
111
157
|
inputs: HydrationFnInput<Context, Params, Skeleton>,
|
|
112
158
|
) => {
|
|
@@ -199,3 +245,16 @@ type Skeleton = {
|
|
|
199
245
|
cursor?: string
|
|
200
246
|
parsedQuery: PostSearchQuery
|
|
201
247
|
}
|
|
248
|
+
|
|
249
|
+
const postSortToV2 = (sort: string | undefined): SearchSortOrder => {
|
|
250
|
+
if (sort === 'top') return SearchSortOrder.TOP
|
|
251
|
+
if (sort === 'latest') return SearchSortOrder.RECENT
|
|
252
|
+
return SearchSortOrder.UNSPECIFIED
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const parseTimestamp = (value: string | undefined): Timestamp | undefined => {
|
|
256
|
+
if (!value) return undefined
|
|
257
|
+
const date = new Date(value)
|
|
258
|
+
if (isNaN(date.getTime())) return undefined
|
|
259
|
+
return Timestamp.fromDate(date)
|
|
260
|
+
}
|
|
@@ -35,6 +35,12 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
35
35
|
labelers,
|
|
36
36
|
includeTakedowns,
|
|
37
37
|
skipViewerBlocks,
|
|
38
|
+
features: ctx.featureGatesClient.scope(
|
|
39
|
+
ctx.featureGatesClient.parseUserContextFromHandler({
|
|
40
|
+
viewer,
|
|
41
|
+
req,
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
38
44
|
})
|
|
39
45
|
const results = await searchStarterPacks({ ...params, hydrateCtx }, ctx)
|
|
40
46
|
return {
|
|
@@ -46,7 +52,7 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
46
52
|
})
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
const
|
|
55
|
+
const skeletonV1 = async (
|
|
50
56
|
inputs: SkeletonFnInput<Context, Params>,
|
|
51
57
|
): Promise<Skeleton> => {
|
|
52
58
|
const { ctx, params } = inputs
|
|
@@ -80,6 +86,34 @@ const skeleton = async (
|
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
const skeletonV2 = async (
|
|
90
|
+
inputs: SkeletonFnInput<Context, Params>,
|
|
91
|
+
): Promise<Skeleton> => {
|
|
92
|
+
const { ctx, params } = inputs
|
|
93
|
+
const { q } = params
|
|
94
|
+
|
|
95
|
+
const res = await ctx.dataplane.searchStarterPacksV2({
|
|
96
|
+
params: {
|
|
97
|
+
query: q,
|
|
98
|
+
viewer: params.hydrateCtx.viewer ?? undefined,
|
|
99
|
+
limit: params.limit,
|
|
100
|
+
cursor: params.cursor,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
return {
|
|
104
|
+
uris: res.starterPacks.map(({ uri }) => uri as AtUriString),
|
|
105
|
+
cursor: parseString(res.pageInfo?.cursor),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const skeleton = async (input: SkeletonFnInput<Context, Params>) => {
|
|
110
|
+
const useV2 = input.params.hydrateCtx.features.checkGate(
|
|
111
|
+
input.params.hydrateCtx.features.Gate.SearchV2Enable,
|
|
112
|
+
)
|
|
113
|
+
const skeletonFn = useV2 ? skeletonV2 : skeletonV1
|
|
114
|
+
return skeletonFn(input)
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
const hydration = async (
|
|
84
118
|
inputs: HydrationFnInput<Context, Params, Skeleton>,
|
|
85
119
|
) => {
|
|
@@ -15,7 +15,17 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
15
15
|
handler: async ({ auth, params, req }) => {
|
|
16
16
|
const viewer = auth.credentials.iss
|
|
17
17
|
const labelers = ctx.reqLabelers(req)
|
|
18
|
-
const
|
|
18
|
+
const features = ctx.featureGatesClient.scope(
|
|
19
|
+
ctx.featureGatesClient.parseUserContextFromHandler({
|
|
20
|
+
viewer,
|
|
21
|
+
req,
|
|
22
|
+
}),
|
|
23
|
+
)
|
|
24
|
+
const hydrateCtx = await ctx.hydrator.createContext({
|
|
25
|
+
viewer,
|
|
26
|
+
labelers,
|
|
27
|
+
features,
|
|
28
|
+
})
|
|
19
29
|
|
|
20
30
|
if (clearlyBadCursor(params.cursor)) {
|
|
21
31
|
return {
|
|
@@ -29,11 +39,23 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
29
39
|
|
|
30
40
|
const query = params.query?.trim() ?? ''
|
|
31
41
|
if (query) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const useV2 = features.checkGate(features.Gate.SearchV2Enable)
|
|
43
|
+
if (useV2) {
|
|
44
|
+
const res = await ctx.dataplane.searchFeedGeneratorsV2({
|
|
45
|
+
params: {
|
|
46
|
+
query,
|
|
47
|
+
viewer: viewer ?? undefined,
|
|
48
|
+
limit: params.limit,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
uris = res.feedGenerators.map(({ uri }) => uri) as AtUriString[]
|
|
52
|
+
} else {
|
|
53
|
+
const res = await ctx.dataplane.searchFeedGenerators({
|
|
54
|
+
query,
|
|
55
|
+
limit: params.limit,
|
|
56
|
+
})
|
|
57
|
+
uris = res.uris as AtUriString[]
|
|
58
|
+
}
|
|
37
59
|
} else {
|
|
38
60
|
const res = await ctx.dataplane.getSuggestedFeeds({
|
|
39
61
|
actorDid: viewer ?? undefined,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Kysely } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
+
await db.schema
|
|
5
|
+
.createTable('post_embed_gallery_image')
|
|
6
|
+
.addColumn('postUri', 'varchar', (col) => col.notNull())
|
|
7
|
+
.addColumn('position', 'varchar', (col) => col.notNull())
|
|
8
|
+
.addColumn('imageCid', 'varchar', (col) => col.notNull())
|
|
9
|
+
.addColumn('alt', 'varchar', (col) => col.notNull())
|
|
10
|
+
.addPrimaryKeyConstraint('post_embed_gallery_image_pkey', [
|
|
11
|
+
'postUri',
|
|
12
|
+
'position',
|
|
13
|
+
])
|
|
14
|
+
.execute()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
18
|
+
await db.schema.dropTable('post_embed_gallery_image').execute()
|
|
19
|
+
}
|
|
@@ -57,3 +57,4 @@ export * as _20250812T183735692Z from './20250812T183735692Z-add-bookmarks.js'
|
|
|
57
57
|
export * as _20250813T174955711Z from './20250813T174955711Z-add-post-agg-bookmarks.js'
|
|
58
58
|
export * as _20251120T004738098Z from './20251120T004738098Z-update-actor-age-assurance-v2.js'
|
|
59
59
|
export * as _20260112T133951271Z from './20260112T133951271Z-add-drafts.js'
|
|
60
|
+
export * as _20260604T224952774Z from './20260604T224952774Z-post-embed-gallery-image.js'
|
|
@@ -2,6 +2,7 @@ export const imageTableName = 'post_embed_image'
|
|
|
2
2
|
export const externalTableName = 'post_embed_external'
|
|
3
3
|
export const recordTableName = 'post_embed_record'
|
|
4
4
|
export const videoTableName = 'post_embed_video'
|
|
5
|
+
export const galleryImageTableName = 'post_embed_gallery_image'
|
|
5
6
|
|
|
6
7
|
export interface PostEmbedImage {
|
|
7
8
|
postUri: string
|
|
@@ -30,9 +31,17 @@ export interface PostEmbedVideo {
|
|
|
30
31
|
alt: string | null
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export interface PostEmbedGalleryImage {
|
|
35
|
+
postUri: string
|
|
36
|
+
position: number
|
|
37
|
+
imageCid: string
|
|
38
|
+
alt: string
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export type PartialDB = {
|
|
34
42
|
[imageTableName]: PostEmbedImage
|
|
35
43
|
[externalTableName]: PostEmbedExternal
|
|
36
44
|
[recordTableName]: PostEmbedRecord
|
|
37
45
|
[videoTableName]: PostEmbedVideo
|
|
46
|
+
[galleryImageTableName]: PostEmbedGalleryImage
|
|
38
47
|
}
|
|
@@ -355,6 +355,14 @@ export class IndexingService {
|
|
|
355
355
|
.deleteFrom('post_embed_record')
|
|
356
356
|
.where('post_embed_record.postUri', 'in', postByUser)
|
|
357
357
|
.execute()
|
|
358
|
+
await this.db.db
|
|
359
|
+
.deleteFrom('post_embed_video')
|
|
360
|
+
.where('post_embed_video.postUri', 'in', postByUser)
|
|
361
|
+
.execute()
|
|
362
|
+
await this.db.db
|
|
363
|
+
.deleteFrom('post_embed_gallery_image')
|
|
364
|
+
.where('post_embed_gallery_image.postUri', 'in', postByUser)
|
|
365
|
+
.execute()
|
|
358
366
|
await this.db.db.deleteFrom('post').where('creator', '=', did).execute()
|
|
359
367
|
await this.db.db
|
|
360
368
|
.deleteFrom('thread_gate')
|
|
@@ -28,6 +28,7 @@ type PostEmbedImage = DatabaseSchemaType['post_embed_image']
|
|
|
28
28
|
type PostEmbedExternal = DatabaseSchemaType['post_embed_external']
|
|
29
29
|
type PostEmbedRecord = DatabaseSchemaType['post_embed_record']
|
|
30
30
|
type PostEmbedVideo = DatabaseSchemaType['post_embed_video']
|
|
31
|
+
type PostEmbedGalleryImage = DatabaseSchemaType['post_embed_gallery_image']
|
|
31
32
|
type PostAncestor = {
|
|
32
33
|
uri: string
|
|
33
34
|
height: number
|
|
@@ -47,6 +48,7 @@ type IndexedPost = {
|
|
|
47
48
|
| PostEmbedExternal
|
|
48
49
|
| PostEmbedRecord
|
|
49
50
|
| PostEmbedVideo
|
|
51
|
+
| PostEmbedGalleryImage[]
|
|
50
52
|
)[]
|
|
51
53
|
ancestors?: PostAncestor[]
|
|
52
54
|
descendents?: PostDescendent[]
|
|
@@ -144,6 +146,7 @@ const insertFn = async (
|
|
|
144
146
|
| PostEmbedExternal
|
|
145
147
|
| PostEmbedRecord
|
|
146
148
|
| PostEmbedVideo
|
|
149
|
+
| PostEmbedGalleryImage[]
|
|
147
150
|
)[] = []
|
|
148
151
|
const postEmbeds = separateEmbeds(obj.embed)
|
|
149
152
|
for (const postEmbed of postEmbeds) {
|
|
@@ -238,6 +241,27 @@ const insertFn = async (
|
|
|
238
241
|
embeds.push(videoEmbed)
|
|
239
242
|
|
|
240
243
|
await db.insertInto('post_embed_video').values(videoEmbed).execute()
|
|
244
|
+
} else if (app.bsky.embed.gallery.$matches(postEmbed)) {
|
|
245
|
+
// Gallery items are a union; today only `#image` exists, but we
|
|
246
|
+
// defensively skip unknown variants for forward-compat.
|
|
247
|
+
const galleryImages: PostEmbedGalleryImage[] = []
|
|
248
|
+
postEmbed.items.forEach((item, i) => {
|
|
249
|
+
if (app.bsky.embed.gallery.image.$matches(item)) {
|
|
250
|
+
galleryImages.push({
|
|
251
|
+
postUri: uri.toString(),
|
|
252
|
+
position: i,
|
|
253
|
+
imageCid: getBlobCidString(item.image),
|
|
254
|
+
alt: item.alt,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
if (galleryImages.length > 0) {
|
|
259
|
+
embeds.push(galleryImages)
|
|
260
|
+
await db
|
|
261
|
+
.insertInto('post_embed_gallery_image')
|
|
262
|
+
.values(galleryImages)
|
|
263
|
+
.execute()
|
|
264
|
+
}
|
|
241
265
|
}
|
|
242
266
|
}
|
|
243
267
|
|
|
@@ -381,8 +405,16 @@ const deleteFn = async (
|
|
|
381
405
|
| PostEmbedImage[]
|
|
382
406
|
| PostEmbedExternal
|
|
383
407
|
| PostEmbedRecord
|
|
408
|
+
| PostEmbedVideo
|
|
409
|
+
| PostEmbedGalleryImage[]
|
|
384
410
|
)[] = []
|
|
385
|
-
const [
|
|
411
|
+
const [
|
|
412
|
+
deletedImgs,
|
|
413
|
+
deletedExternals,
|
|
414
|
+
deletedPosts,
|
|
415
|
+
deletedVideo,
|
|
416
|
+
deletedGalleryImgs,
|
|
417
|
+
] = await Promise.all([
|
|
386
418
|
db
|
|
387
419
|
.deleteFrom('post_embed_image')
|
|
388
420
|
.where('postUri', '=', uriStr)
|
|
@@ -398,6 +430,16 @@ const deleteFn = async (
|
|
|
398
430
|
.where('postUri', '=', uriStr)
|
|
399
431
|
.returningAll()
|
|
400
432
|
.executeTakeFirst(),
|
|
433
|
+
db
|
|
434
|
+
.deleteFrom('post_embed_video')
|
|
435
|
+
.where('postUri', '=', uriStr)
|
|
436
|
+
.returningAll()
|
|
437
|
+
.executeTakeFirst(),
|
|
438
|
+
db
|
|
439
|
+
.deleteFrom('post_embed_gallery_image')
|
|
440
|
+
.where('postUri', '=', uriStr)
|
|
441
|
+
.returningAll()
|
|
442
|
+
.execute(),
|
|
401
443
|
])
|
|
402
444
|
if (deletedImgs.length) {
|
|
403
445
|
deletedEmbeds.push(deletedImgs)
|
|
@@ -405,6 +447,12 @@ const deleteFn = async (
|
|
|
405
447
|
if (deletedExternals) {
|
|
406
448
|
deletedEmbeds.push(deletedExternals)
|
|
407
449
|
}
|
|
450
|
+
if (deletedVideo) {
|
|
451
|
+
deletedEmbeds.push(deletedVideo)
|
|
452
|
+
}
|
|
453
|
+
if (deletedGalleryImgs.length) {
|
|
454
|
+
deletedEmbeds.push(deletedGalleryImgs)
|
|
455
|
+
}
|
|
408
456
|
if (deletedPosts) {
|
|
409
457
|
const embedUri = new AtUri(deletedPosts.embedUri)
|
|
410
458
|
deletedEmbeds.push(deletedPosts)
|