diff --git a/package.json b/package.json index 9a3f4b42993..257781f6fc1 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@swc/jest": "^0.2.24", "@types/jest": "^28.1.4", "@types/node": "^18.0.0", - "@typescript-eslint/eslint-plugin": "^5.38.1", - "@typescript-eslint/parser": "^5.38.1", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", "babel-eslint": "^10.1.0", "dotenv": "^16.0.3", "esbuild": "^0.14.48", @@ -47,7 +47,7 @@ "prettier": "^2.7.1", "prettier-config-standard": "^5.0.0", "ts-node": "^10.8.2", - "typescript": "^4.8.4" + "typescript": "^5.3.3" }, "workspaces": { "packages": [ diff --git a/packages/bsky/buf.gen.yaml b/packages/bsky/buf.gen.yaml index 47bbfcd4f64..240c3cd548a 100644 --- a/packages/bsky/buf.gen.yaml +++ b/packages/bsky/buf.gen.yaml @@ -1,8 +1,13 @@ version: v1 plugins: - plugin: es - opt: target=ts + opt: + - target=ts + - import_extension=.ts + out: src/data-plane/gen - plugin: connect-es - opt: target=ts + opt: + - target=ts + - import_extension=.ts out: src/data-plane/gen diff --git a/packages/bsky/proto/bsky.proto b/packages/bsky/proto/bsky.proto index ad273562773..db89d87ecf1 100644 --- a/packages/bsky/proto/bsky.proto +++ b/packages/bsky/proto/bsky.proto @@ -9,6 +9,112 @@ import "google/protobuf/timestamp.proto"; // Bsky // +// +// Records +// + +message Record { + bytes record = 1; + string cid = 2; + google.protobuf.Timestamp indexed_at = 4; + bool taken_down = 5; +} + + +message GetBlockRecordsRequest { + repeated string uris = 1; +} + +message GetBlockRecordsResponse { + repeated Record records = 1; +} + +message GetFeedGeneratorRecordsRequest { + repeated string uris = 1; +} + +message GetFeedGeneratorRecordsResponse { + repeated Record records = 1; +} + +message GetFollowRecordsRequest { + repeated string uris = 1; +} + +message GetFollowRecordsResponse { + repeated Record records = 1; +} + +message GetLikeRecordsRequest { + repeated string uris = 1; +} + +message GetLikeRecordsResponse { + repeated Record records = 1; +} + +message GetListBlockRecordsRequest { + repeated string uris = 1; +} + +message GetListBlockRecordsResponse { + repeated Record records = 1; +} + +message GetListItemRecordsRequest { + repeated string uris = 1; +} + +message GetListItemRecordsResponse { + repeated Record records = 1; +} + +message GetListRecordsRequest { + repeated string uris = 1; +} + +message GetListRecordsResponse { + repeated Record records = 1; +} + +message PostRecordMeta { + bool violates_thread_gate = 1; +} + +message GetPostRecordsRequest { + repeated string uris = 1; +} + +message GetPostRecordsResponse { + repeated Record records = 1; + repeated PostRecordMeta meta = 2; +} + +message GetProfileRecordsRequest { + repeated string uris = 1; +} + +message GetProfileRecordsResponse { + repeated Record records = 1; +} + +message GetRepostRecordsRequest { + repeated string uris = 1; +} + +message GetRepostRecordsResponse { + repeated Record records = 1; +} + +message GetThreadGateRecordsRequest { + repeated string uris = 1; +} + +message GetThreadGateRecordsResponse { + repeated Record records = 1; +} + + // // Follows // @@ -32,8 +138,13 @@ message GetFollowersRequest { string cursor = 3; } +message FollowInfo { + string uri = 1; + string did = 2; +} + message GetFollowersResponse { - repeated string uris = 1; + repeated FollowInfo followers = 1; string cursor = 2; } @@ -46,28 +157,28 @@ message GetFollowsRequest { } message GetFollowsResponse { - repeated string uris = 1; + repeated FollowInfo follows = 1; string cursor = 2; } // - Return number of users who follow A // - For `followersCount` on a profile -message GetFollowersCountRequest { - string actor_did = 1; +message GetFollowerCountsRequest { + repeated string dids = 1; } -message GetFollowersCountResponse { - int32 count = 1; +message GetFollowerCountsResponse { + repeated int32 counts = 1; } // - Return number of users followed by A // - For `followCount` on a profile -message GetFollowsCountRequest { - string actor_did = 1; +message GetFollowCountsRequest { + repeated string dids = 1; } -message GetFollowsCountResponse { - int32 count = 1; +message GetFollowCountsResponse { + repeated int32 counts = 1; } // @@ -77,7 +188,7 @@ message GetFollowsCountResponse { // - return like uris where subject uri is subject A // - `getLikes` list for a post message GetLikesBySubjectRequest { - string subject_uri = 1; + RecordRef subject = 1; int32 limit = 2; string cursor = 3; } @@ -87,15 +198,15 @@ message GetLikesBySubjectResponse { string cursor = 2; } -// - return like uri for user A on subject B +// - return like uris for user A on subject B, C, D... // - viewer state on posts -message GetLikeByActorAndSubjectRequest { +message GetLikesByActorAndSubjectsRequest { string actor_did = 1; - string subject_uri = 2; + repeated RecordRef refs = 2; } -message GetLikeByActorAndSubjectResponse { - string uri = 1; +message GetLikesByActorAndSubjectsResponse { + repeated string uris = 1; } // - return recent like uris for user A @@ -106,19 +217,24 @@ message GetActorLikesRequest { string cursor = 3; } +message LikeInfo { + string uri = 1; + string subject = 2; +} + message GetActorLikesResponse { - repeated string uris = 1; + repeated LikeInfo likes = 1; string cursor = 2; } -// - return number of likes on subject A +// - return number of likes on subjects A, B, C... // - post or feed generator hydration `likeCount` field -message GetLikesCountRequest { - string subject_uri = 1; +message GetLikeCountsRequest { + repeated string uris = 1; } -message GetLikesCountResponse { - int32 count = 1; +message GetLikeCountsResponse { + repeated int32 counts = 1; } // @@ -129,8 +245,9 @@ message GetLikesCountResponse { // - `getReposts` list for a post message GetRepostsBySubjectRequest { string subject_uri = 1; - int32 limit = 2; - string cursor = 3; + optional string subject_cid = 2; + int32 limit = 3; + string cursor = 4; } message GetRepostsBySubjectResponse { @@ -138,15 +255,20 @@ message GetRepostsBySubjectResponse { string cursor = 2; } -// - return repost uri for user A on subject B +// - return repost uris for user A on subject B, C, D... // - viewer state on posts -message GetRepostByActorAndSubjectRequest { +message GetRepostsByActorAndSubjectsRequest { string actor_did = 1; - string subject_uri = 2; + repeated RecordRef refs = 2; } -message GetRepostByActorAndSubjectResponse { +message RecordRef { string uri = 1; + string cid = 2; +} + +message GetRepostsByActorAndSubjectsResponse { + repeated string uris = 1; } // - return recent repost uris for user A @@ -164,37 +286,34 @@ message GetActorRepostsResponse { // - return number of reposts on subject A // - post or feed generator hydration `repostCount` field -message GetRepostsCountRequest { - string subject_uri = 1; +message GetRepostCountsRequest { + repeated string uris = 1; } -message GetRepostsCountResponse { - int32 count = 1; +message GetRepostCountsResponse { + repeated int32 counts = 1; } // // Profile // -// - return profile record for dids A, B, C… +// - return actor information for dids A, B, C… // - profile hydration // - should this include handles? apply repo takedown? -message GetProfilesRequest { +message GetActorsRequest { repeated string dids = 1; } -message GetProfilesResponse { - repeated bytes records = 1; -} - -// - return handle for dids A, B, C… -// - profile hydration -message GetHandlesRequest { - repeated string dids = 1; +message ActorInfo { + bool exists = 1; + string handle = 2; + Record profile = 3; + bool taken_down = 4; } -message GetHandlesResponse { - repeated string handles = 1; +message GetActorsResponse { + repeated ActorInfo actors = 1; } // - return did for handle A @@ -208,10 +327,58 @@ message GetDidsByHandlesResponse { repeated string dids = 1; } +// +// Relationships +// + +// - return relationships between user A and users B, C, D... +// - profile hydration +// - block application +message GetRelationshipsRequest { + string actor_did = 1; + repeated string target_dids = 2; +} + +message Relationships { + bool muted = 1; + string muted_by_list = 2; + string blocked_by = 3; + string blocking = 4; + string blocked_by_list = 5; + string blocking_by_list = 6; + string following = 7; + string followed_by = 8; +} + +message GetRelationshipsResponse { + repeated Relationships relationships = 1; +} + +// - return whether a block (bidrectionally and either direct or through a list) exists between two dids +// - enforcing 3rd party block violations +message RelationshipPair { + string a = 1; + string b = 2; +} + +message GetBlockExistenceRequest { + repeated RelationshipPair pairs = 1; +} + +message GetBlockExistenceResponse { + repeated bool exists = 1; +} + + // // Lists // +message ListItemInfo { + string uri = 1; + string did = 2; +} + // - Return dids of users in list A // - E.g. to view items in one of your mute lists message GetListMembersRequest { @@ -221,7 +388,7 @@ message GetListMembersRequest { } message GetListMembersResponse { - repeated string dids = 1; + repeated ListItemInfo listitems = 1; string cursor = 2; } @@ -236,16 +403,6 @@ message GetListMembershipResponse { repeated string listitem_uris = 1; } -// - Return list record for list uri -// - list view hydration -message GetListRequest { - string list_uri = 1; -} - -message GetListResponse { - bytes record = 1; -} - // - Return number of items in list A // - For aggregate message GetListCountRequest { @@ -256,6 +413,20 @@ message GetListCountResponse { int32 count = 1; } + +// - return list of uris of lists created by A +// - `getLists` +message GetActorListsRequest { + string actor_did = 1; + int32 limit = 2; + string cursor = 3; +} + +message GetActorListsResponse { + repeated string list_uris = 1; + string cursor = 2; +} + // // Mutes // @@ -377,7 +548,7 @@ message GetBlocklistSubscriptionRequest { } message GetBlocklistSubscriptionResponse { - bool subscribed = 1; + string listblock_uri = 1; } // - return list of list uris of Blockslists that A subscribes to @@ -409,7 +580,8 @@ message GetNotificationsRequest { message Notification { string uri = 1; string reason = 2; - google.protobuf.Timestamp timestamp = 3; + string reason_subject = 3; + google.protobuf.Timestamp timestamp = 4; } message GetNotificationsResponse { @@ -450,16 +622,6 @@ message GetUnreadNotificationCountResponse { // FeedGenerators // -// - Returns feed generator records with uris A, B, C… -// - hydration of feed generator views -message GetFeedGeneratorsRequest { - repeated string uris = 1; -} - -message GetFeedGeneratorsResponse { - repeated bytes records = 1; -} - // - Return uris of feed generator records created by user A // - `getActorFeeds` message GetActorFeedsRequest { @@ -508,12 +670,12 @@ message GetAuthorFeedRequest { string actor_did = 1; int32 limit = 2; string cursor = 3; - bool replies_only = 4; + bool no_replies = 4; bool media_only = 5; } message GetAuthorFeedResponse { - repeated string uris = 1; + repeated FeedItem items = 1; string cursor = 2; } @@ -526,10 +688,15 @@ message GetTimelineRequest { } message GetTimelineResponse { - repeated string uris = 1; + repeated FeedItem items = 1; string cursor = 2; } +message FeedItem { + string uri = 1; + string repost = 2; +} + // - Return recent post uris from users in list A // - `getListFeed` // - (This is essentially the same as `getTimeline` but instead of follows of a did, it is list items of a list) @@ -559,15 +726,6 @@ message GetThreadResponse { repeated string uris = 1; } -// Return threadgate records with uris A, B, C… -message GetThreadgatesRequest { - repeated string uris = 1; -} - -message GetThreadgatesResponse { - repeated bytes records = 1; -} - // // Search // @@ -603,14 +761,15 @@ message SearchPostsResponse { // // - Return DIDs of suggested follows for a user, excluding anyone they already follow -// - `getSuggestions` -message GetSuggestionsRequest { +// - `getSuggestions`, `getSuggestedFollowsByActor` +message GetFollowSuggestionsRequest { string actor_did = 1; - int32 limit = 2; - string cursor = 3; + string relative_to_did = 2; + int32 limit = 3; + string cursor = 4; } -message GetSuggestionsResponse { +message GetFollowSuggestionsResponse { repeated string dids = 1; string cursor = 2; } @@ -619,23 +778,23 @@ message GetSuggestionsResponse { // Posts // -// - Return post records with uris A, B, C… +// - Return post reply count with uris A, B, C… // - All feed hydration -message GetPostsRequest { +message GetPostReplyCountsRequest { repeated string uris = 1; } -message GetPostsResponse { - repeated bytes records = 1; +message GetPostReplyCountsResponse { + repeated int32 counts = 1; } -// - Return post reply count with uris A, B, C… -// - All feed hydration -message GetPostReplyCountRequest { - repeated string uris = 1; +// - Return post count for users A, B, C… +// - Profile hydration +message GetPostCountsRequest { + repeated string dids = 1; } -message GetPostReplyCountResponse { +message GetPostCountsResponse { repeated int32 counts = 1; } @@ -651,7 +810,7 @@ message GetLabelsRequest { } message GetLabelsResponse { - repeated bytes records = 1; + repeated bytes labels = 1; } // @@ -697,34 +856,50 @@ message PingRequest {} message PingResponse {} service Service { + // Records + rpc GetBlockRecords(GetBlockRecordsRequest) returns (GetBlockRecordsResponse); + rpc GetFeedGeneratorRecords(GetFeedGeneratorRecordsRequest) returns (GetFeedGeneratorRecordsResponse); + rpc GetFollowRecords(GetFollowRecordsRequest) returns (GetFollowRecordsResponse); + rpc GetLikeRecords(GetLikeRecordsRequest) returns (GetLikeRecordsResponse); + rpc GetListBlockRecords(GetListBlockRecordsRequest) returns (GetListBlockRecordsResponse); + rpc GetListItemRecords(GetListItemRecordsRequest) returns (GetListItemRecordsResponse); + rpc GetListRecords(GetListRecordsRequest) returns (GetListRecordsResponse); + rpc GetPostRecords(GetPostRecordsRequest) returns (GetPostRecordsResponse); + rpc GetProfileRecords(GetProfileRecordsRequest) returns (GetProfileRecordsResponse); + rpc GetRepostRecords(GetRepostRecordsRequest) returns (GetRepostRecordsResponse); + rpc GetThreadGateRecords(GetThreadGateRecordsRequest) returns (GetThreadGateRecordsResponse); + // Follows rpc GetActorFollowsActors(GetActorFollowsActorsRequest) returns (GetActorFollowsActorsResponse); rpc GetFollowers(GetFollowersRequest) returns (GetFollowersResponse); rpc GetFollows(GetFollowsRequest) returns (GetFollowsResponse); - rpc GetFollowersCount(GetFollowersCountRequest) returns (GetFollowersCountResponse); - rpc GetFollowsCount(GetFollowsCountRequest) returns (GetFollowsCountResponse); + rpc GetFollowerCounts(GetFollowerCountsRequest) returns (GetFollowerCountsResponse); + rpc GetFollowCounts(GetFollowCountsRequest) returns (GetFollowCountsResponse); // Likes rpc GetLikesBySubject(GetLikesBySubjectRequest) returns (GetLikesBySubjectResponse); - rpc GetLikeByActorAndSubject(GetLikeByActorAndSubjectRequest) returns (GetLikeByActorAndSubjectResponse); + rpc GetLikesByActorAndSubjects(GetLikesByActorAndSubjectsRequest) returns (GetLikesByActorAndSubjectsResponse); rpc GetActorLikes(GetActorLikesRequest) returns (GetActorLikesResponse); - rpc GetLikesCount(GetLikesCountRequest) returns (GetLikesCountResponse); + rpc GetLikeCounts(GetLikeCountsRequest) returns (GetLikeCountsResponse); // Reposts rpc GetRepostsBySubject(GetRepostsBySubjectRequest) returns (GetRepostsBySubjectResponse); - rpc GetRepostByActorAndSubject(GetRepostByActorAndSubjectRequest) returns (GetRepostByActorAndSubjectResponse); + rpc GetRepostsByActorAndSubjects(GetRepostsByActorAndSubjectsRequest) returns (GetRepostsByActorAndSubjectsResponse); rpc GetActorReposts(GetActorRepostsRequest) returns (GetActorRepostsResponse); - rpc GetRepostsCount(GetRepostsCountRequest) returns (GetRepostsCountResponse); + rpc GetRepostCounts(GetRepostCountsRequest) returns (GetRepostCountsResponse); // Profile - rpc GetProfiles(GetProfilesRequest) returns (GetProfilesResponse); - rpc GetHandles(GetHandlesRequest) returns (GetHandlesResponse); + rpc GetActors(GetActorsRequest) returns (GetActorsResponse); rpc GetDidsByHandles(GetDidsByHandlesRequest) returns (GetDidsByHandlesResponse); + // Relationships + rpc GetRelationships(GetRelationshipsRequest) returns (GetRelationshipsResponse); + rpc GetBlockExistence(GetBlockExistenceRequest) returns (GetBlockExistenceResponse); + // Lists + rpc GetActorLists(GetActorListsRequest) returns (GetActorListsResponse); rpc GetListMembers(GetListMembersRequest) returns (GetListMembersResponse); rpc GetListMembership(GetListMembershipRequest) returns (GetListMembershipResponse); - rpc GetList(GetListRequest) returns (GetListResponse); rpc GetListCount(GetListCountRequest) returns (GetListCountResponse); // Mutes @@ -752,7 +927,6 @@ service Service { rpc UpdateNotificationSeen(UpdateNotificationSeenRequest) returns (UpdateNotificationSeenResponse); // FeedGenerators - rpc GetFeedGenerators(GetFeedGeneratorsRequest) returns (GetFeedGeneratorsResponse); rpc GetActorFeeds(GetActorFeedsRequest) returns (GetActorFeedsResponse); rpc GetSuggestedFeeds(GetSuggestedFeedsRequest) returns (GetSuggestedFeedsResponse); rpc GetFeedGeneratorStatus(GetFeedGeneratorStatusRequest) returns (GetFeedGeneratorStatusResponse); @@ -764,18 +938,17 @@ service Service { // Threads rpc GetThread(GetThreadRequest) returns (GetThreadResponse); - rpc GetThreadgates(GetThreadgatesRequest) returns (GetThreadgatesResponse); // Search rpc SearchActors(SearchActorsRequest) returns (SearchActorsResponse); rpc SearchPosts(SearchPostsRequest) returns (SearchPostsResponse); // Suggestions - rpc GetSuggestions(GetSuggestionsRequest) returns (GetSuggestionsResponse); + rpc GetFollowSuggestions(GetFollowSuggestionsRequest) returns (GetFollowSuggestionsResponse); // Posts - rpc GetPosts(GetPostsRequest) returns (GetPostsResponse); - rpc GetPostReplyCount(GetPostReplyCountRequest) returns (GetPostReplyCountResponse); + rpc GetPostReplyCounts(GetPostReplyCountsRequest) returns (GetPostReplyCountsResponse); + rpc GetPostCounts(GetPostCountsRequest) returns (GetPostCountsResponse); // Labels rpc GetLabels(GetLabelsRequest) returns (GetLabelsResponse); @@ -786,7 +959,7 @@ service Service { // Moderation rpc GetBlobTakedown(GetBlobTakedownRequest) returns (GetBlobTakedownResponse); rpc UpdateTakedown(UpdateTakedownRequest) returns (UpdateTakedownResponse); - + // Ping rpc Ping(PingRequest) returns (PingResponse); } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfile.ts b/packages/bsky/src/api/app/bsky/actor/getProfile.ts index 09699b8914b..e5868275b93 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfile.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfile.ts @@ -1,100 +1,86 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfile' -import { softDeleted } from '../../../../db/util' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { Actor } from '../../../../db/tables/actor' -import { - ActorService, - ProfileDetailHydrationState, -} from '../../../../services/actor' import { setRepoRev } from '../../../util' import { createPipeline, noRules } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.actor.getProfile({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ auth, params, res }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) const viewer = 'did' in auth.credentials ? auth.credentials.did : null const canViewTakendownProfile = auth.credentials.type === 'role' && auth.credentials.triage - const [result, repoRev] = await Promise.allSettled([ - getProfile( - { ...params, viewer, canViewTakendownProfile }, - { db, actorService }, - ), - actorService.getRepoRev(viewer), + const [result, repoRev] = await Promise.all([ + getProfile({ ...params, viewer, canViewTakendownProfile }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) - if (repoRev.status === 'fulfilled') { - setRepoRev(res, repoRev.value) - } - if (result.status === 'rejected') { - throw result.reason - } + setRepoRev(res, repoRev) return { encoding: 'application/json', - body: result.value, + body: result, } }, }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { actorService } = ctx - const { canViewTakendownProfile } = params - const actor = await actorService.getActor(params.actor, true) - if (!actor) { +const skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { throw new InvalidRequestError('Profile not found') } - if (!canViewTakendownProfile && softDeleted(actor)) { - throw new InvalidRequestError( - 'Account has been taken down', - 'AccountTakedown', - ) - } - return { params, actor } + return { did } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const { params, actor } = state - const { viewer, canViewTakendownProfile } = params - const hydration = await actorService.views.profileDetailHydration( - [actor.did], - { viewer, includeSoftDeleted: canViewTakendownProfile }, +const hydration = async (input: { + ctx: Context + params: Params + skeleton: SkeletonState +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed( + [skeleton.did], + params.viewer, + true, ) - return { ...state, ...hydration } } -const presentation = (state: HydrationState, ctx: Context) => { - const { actorService } = ctx - const { params, actor } = state - const { viewer } = params - const profiles = actorService.views.profileDetailPresentation( - [actor.did], - state, - { viewer }, - ) - const profile = profiles[actor.did] +const presentation = (input: { + ctx: Context + params: Params + skeleton: SkeletonState + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = input + const profile = ctx.views.profileDetailed(skeleton.did, hydration) if (!profile) { throw new InvalidRequestError('Profile not found') + } else if ( + !params.canViewTakendownProfile && + ctx.views.actorIsTakendown(skeleton.did, hydration) + ) { + throw new InvalidRequestError( + 'Account has been taken down', + 'AccountTakedown', + ) } return profile } type Context = { - db: Database - actorService: ActorService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -102,6 +88,4 @@ type Params = QueryParams & { canViewTakendownProfile: boolean } -type SkeletonState = { params: Params; actor: Actor } - -type HydrationState = SkeletonState & ProfileDetailHydrationState +type SkeletonState = { did: string } diff --git a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts index f2e0eb3fd50..1a754443e3b 100644 --- a/packages/bsky/src/api/app/bsky/actor/getProfiles.ts +++ b/packages/bsky/src/api/app/bsky/actor/getProfiles.ts @@ -2,13 +2,10 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getProfiles' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { - ActorService, - ProfileDetailHydrationState, -} from '../../../../services/actor' import { setRepoRev } from '../../../util' import { createPipeline, noRules } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getProfile = createPipeline(skeleton, hydration, noRules, presentation) @@ -20,7 +17,7 @@ export default function (server: Server, ctx: AppContext) { const viewer = auth.credentials.did const [result, repoRev] = await Promise.all([ - getProfile({ ...params, viewer }, { db, actorService }), + getProfile({ ...params, viewer }, ctx), actorService.getRepoRev(viewer), ]) @@ -34,45 +31,44 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { actorService } = ctx - const actors = await actorService.getActors(params.actors) - return { params, dids: actors.map((a) => a.did) } +const skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + const dids = await ctx.hydrator.actor.getDidsDefined(params.actors) + return { dids } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const { params, dids } = state - const { viewer } = params - const hydration = await actorService.views.profileDetailHydration(dids, { - viewer, - }) - return { ...state, ...hydration } +const hydration = async (input: { + ctx: Context + params: Params + skeleton: SkeletonState +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed(skeleton.dids, params.viewer) } -const presentation = (state: HydrationState, ctx: Context) => { - const { actorService } = ctx - const { params, dids } = state - const { viewer } = params - const profiles = actorService.views.profileDetailPresentation(dids, state, { - viewer, - }) - const profileViews = mapDefined(dids, (did) => profiles[did]) - return { profiles: profileViews } +const presentation = (input: { + ctx: Context + params: Params + skeleton: SkeletonState + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + const profiles = mapDefined(skeleton.dids, (did) => + ctx.views.profileDetailed(did, hydration), + ) + return { profiles } } type Context = { - db: Database - actorService: ActorService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; dids: string[] } - -type HydrationState = SkeletonState & ProfileDetailHydrationState +type SkeletonState = { dids: string[] } diff --git a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts index f68ba68eb66..7783f0ac753 100644 --- a/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts +++ b/packages/bsky/src/api/app/bsky/actor/getSuggestions.ts @@ -1,13 +1,12 @@ import { mapDefined } from '@atproto/common' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { Actor } from '../../../../db/tables/actor' -import { notSoftDeletedClause } from '../../../../db/util' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/getSuggestions' import { createPipeline } from '../../../../pipeline' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { const getSuggestions = createPipeline( @@ -19,15 +18,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.actor.getSuggestions({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.did - - const result = await getSuggestions( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getSuggestions({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -37,114 +29,86 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { viewer } = params - const alreadyIncluded = parseCursor(params.cursor) - const { ref } = db.db.dynamic - const suggestions = await db.db - .selectFrom('suggested_follow') - .innerJoin('actor', 'actor.did', 'suggested_follow.did') - .where(notSoftDeletedClause(ref('actor'))) - .where('suggested_follow.did', '!=', viewer ?? '') - .whereNotExists((qb) => - qb - .selectFrom('follow') - .selectAll() - .where('creator', '=', viewer ?? '') - .whereRef('subjectDid', '=', ref('actor.did')), - ) - .if(alreadyIncluded.length > 0, (qb) => - qb.where('suggested_follow.order', 'not in', alreadyIncluded), - ) - .selectAll() - .orderBy('suggested_follow.order', 'asc') - .execute() - - // always include first two - const firstTwo = suggestions.filter( - (row) => row.order === 1 || row.order === 2, - ) - const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2) - const limited = firstTwo.concat(shuffle(rest)).slice(0, params.limit) - - // if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle - const cursor = - limited.length > 0 - ? limited - .map((row) => row.order.toString()) - .concat(alreadyIncluded.map((id) => id.toString())) - .join(':') - : undefined - - return { params, suggestions: limited, cursor } -} - -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, suggestions } = state - const { viewer } = params - const [actors, bam] = await Promise.all([ - actorService.views.profiles(suggestions, viewer), - graphService.getBlockAndMuteState( - viewer ? suggestions.map((sug) => [viewer, sug.did]) : [], - ), - ]) - return { ...state, bam, actors } +const skeleton = async (input: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = input + let dids: string[] = [] + let cursor: string | undefined = params.cursor + // filter out follows and re-fetch if left with an empty page + while (dids.length === 0) { + const suggestions = await ctx.dataplane.getFollowSuggestions({ + actorDid: params.viewer ?? undefined, + cursor, + limit: params.limit, + }) + dids = suggestions.dids + cursor = parseString(suggestions.cursor) + if (!cursor || params.viewer === null) { + break + } + const follows = await ctx.dataplane.getActorFollowsActors({ + actorDid: params.viewer, + targetDids: dids, + }) + dids = dids.filter((did, i) => !follows.uris[i] && did !== params.viewer) + } + return { dids, cursor } } -const noBlocksOrMutes = (state: HydrationState) => { - const { viewer } = state.params - if (!viewer) return state - state.suggestions = state.suggestions.filter( - (item) => - !state.bam.block([viewer, item.did]) && - !state.bam.mute([viewer, item.did]), +const hydration = async (input: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = input + return ctx.hydrator.hydrateProfilesDetailed( + skeleton.dids, + params.viewer, + true, ) - return state } -const presentation = (state: HydrationState) => { - const { suggestions, actors, cursor } = state - const suggestedActors = mapDefined(suggestions, (sug) => actors[sug.did]) - return { actors: suggestedActors, cursor } +const noBlocksOrMutes = (input: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + skeleton.dids = skeleton.dids.filter( + (did) => + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration), + ) + return skeleton } -const parseCursor = (cursor?: string): number[] => { - if (!cursor) { - return [] - } - try { - return cursor - .split(':') - .map((id) => parseInt(id, 10)) - .filter((id) => !isNaN(id)) - } catch { - return [] +const presentation = (input: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = input + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profile(did, hydration), + ) + return { + actors, + cursor: skeleton.cursor, } } -const shuffle = (arr: T[]): T[] => { - return arr - .map((value) => ({ value, sort: Math.random() })) - .sort((a, b) => a.sort - b.sort) - .map(({ value }) => value) -} - type Context = { - db: Database - actorService: ActorService - graphService: GraphService + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views } -type Params = QueryParams & { viewer: string | null } - -type SkeletonState = { params: Params; suggestions: Actor[]; cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap +type Params = QueryParams & { + viewer: string | null } + +type Skeleton = { dids: string[]; cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/actor/searchActors.ts b/packages/bsky/src/api/app/bsky/actor/searchActors.ts index 66e934ac0b3..5a207d02667 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActors.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActors.ts @@ -1,64 +1,97 @@ -import { sql } from 'kysely' import AppContext from '../../../../context' import { Server } from '../../../../lexicon' +import { mapDefined } from '@atproto/common' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActors' import { - cleanQuery, - getUserSearchQuery, - SearchKeyset, -} from '../../../../services/util/search' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { cleanQuery } from '../../../../services/util/search' export default function (server: Server, ctx: AppContext) { + const searchActors = createPipeline( + skeleton, + hydration, + noBlocks, + presentation, + ) server.app.bsky.actor.searchActors({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - const { cursor, limit } = params - const requester = auth.credentials.did - const rawQuery = params.q ?? params.term - const query = cleanQuery(rawQuery || '') - const db = ctx.db.getReplica('search') - - let results: string[] - let resCursor: string | undefined - if (ctx.searchAgent) { - const res = - await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ - q: query, - cursor, - limit, - }) - results = res.data.actors.map((a) => a.did) - resCursor = res.data.cursor - } else { - const res = query - ? await getUserSearchQuery(db, { query, limit, cursor }) - .select('distance') - .selectAll('actor') - .execute() - : [] - results = res.map((a) => a.did) - const keyset = new SearchKeyset(sql``, sql``) - resCursor = keyset.packFromResult(res) - } - - const actors = await ctx.services - .actor(db) - .views.profiles(results, requester) - - const SKIP = [] - const filtered = results.flatMap((did) => { - const actor = actors[did] - if (!actor) return SKIP - if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP - return actor - }) - + const viewer = auth.credentials.did + const results = await searchActors({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { - cursor: resCursor, - actors: filtered, - }, + body: results, } }, }) } + +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const rawQuery = params.q ?? params.term + const term = cleanQuery(rawQuery || '') + + // @TODO + // add hits total + + const res = await ctx.dataplane.searchActors({ + term, + limit: params.limit, + cursor: params.cursor, + }) + return { + dids: res.dids, + cursor: parseString(res.cursor), + } +} + +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateProfiles(skeleton.dids, params.viewer) +} + +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.dids = skeleton.dids.filter( + (did) => !ctx.views.viewerBlockExists(did, hydration), + ) + return skeleton +} + +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profile(did, hydration), + ) + return { + actors, + cursor: skeleton.cursor, + } +} + +type Context = { + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { viewer: string | null } + +type Skeleton = { + dids: string[] + hitsTotal?: number + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts index ff674edf898..9ed622ecf55 100644 --- a/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts +++ b/packages/bsky/src/api/app/bsky/actor/searchActorsTypeahead.ts @@ -1,56 +1,94 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' +import { mapDefined } from '@atproto/common' +import { QueryParams } from '../../../../lexicon/types/app/bsky/actor/searchActorsTypeahead' import { - cleanQuery, - getUserSearchQuerySimple, -} from '../../../../services/util/search' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { cleanQuery } from '../../../../services/util/search' export default function (server: Server, ctx: AppContext) { + const searchActorsTypeahead = createPipeline( + skeleton, + hydration, + noBlocks, + presentation, + ) server.app.bsky.actor.searchActorsTypeahead({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { limit } = params - const requester = auth.credentials.did - const rawQuery = params.q ?? params.term - const query = cleanQuery(rawQuery || '') - const db = ctx.db.getReplica('search') - - let results: string[] - if (ctx.searchAgent) { - const res = - await ctx.searchAgent.api.app.bsky.unspecced.searchActorsSkeleton({ - q: query, - typeahead: true, - limit, - }) - results = res.data.actors.map((a) => a.did) - } else { - const res = query - ? await getUserSearchQuerySimple(db, { query, limit }) - .selectAll('actor') - .execute() - : [] - results = res.map((a) => a.did) - } - - const actors = await ctx.services - .actor(db) - .views.profilesBasic(results, requester, { omitLabels: true }) - - const SKIP = [] - const filtered = results.flatMap((did) => { - const actor = actors[did] - if (!actor) return SKIP - if (actor.viewer?.blocking || actor.viewer?.blockedBy) return SKIP - return actor - }) - + const viewer = auth.credentials.did + const results = await searchActorsTypeahead({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { - actors: filtered, - }, + body: results, } }, }) } + +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const rawQuery = params.q ?? params.term + const term = cleanQuery(rawQuery || '') + + // @TODO + // add typeahead option + // add hits total + + const res = await ctx.dataplane.searchActors({ + term, + limit: params.limit, + }) + return { + dids: res.dids, + cursor: parseString(res.cursor), + } +} + +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateProfilesBasic(skeleton.dids, params.viewer) +} + +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.dids = skeleton.dids.filter( + (did) => !ctx.views.viewerBlockExists(did, hydration), + ) + return skeleton +} + +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const actors = mapDefined(skeleton.dids, (did) => + ctx.views.profileBasic(did, hydration), + ) + return { + actors, + } +} + +type Context = { + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { viewer: string | null } + +type Skeleton = { + dids: string[] +} diff --git a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts index 7a28e4efe67..26319ab52b9 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorFeeds.ts @@ -1,63 +1,87 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorFeeds' import AppContext from '../../../../context' -import { TimeCidKeyset, paginate } from '../../../../db/pagination' +import { createPipeline, noRules } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { + const getActorFeeds = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.feed.getActorFeeds({ auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { - const { actor, limit, cursor } = params const viewer = auth.credentials.did - - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) + const result = await getActorFeeds({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, } + }, + }) +} - const { ref } = db.db.dynamic - let feedsQb = feedService - .selectFeedGeneratorQb(viewer) - .where('feed_generator.creator', '=', creatorRes.did) +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { + throw new InvalidRequestError('Profile not found') + } + const feedsRes = await ctx.dataplane.getActorFeeds({ + actorDid: did, + cursor: params.cursor, + limit: params.limit, + }) + return { + feedUris: feedsRes.uris, + cursor: parseString(feedsRes.cursor), + } +} - const keyset = new TimeCidKeyset( - ref('feed_generator.createdAt'), - ref('feed_generator.cid'), - ) - feedsQb = paginate(feedsQb, { - limit, - cursor, - keyset, - }) +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedGens(skeleton.feedUris, params.viewer) +} - const [feedsRes, profiles] = await Promise.all([ - feedsQb.execute(), - actorService.views.profiles([creatorRes], viewer), - ]) - if (!profiles[creatorRes.did]) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feeds = mapDefined(skeleton.feedUris, (uri) => + ctx.views.feedGenerator(uri, hydration), + ) + return { + feeds, + cursor: skeleton.cursor, + } +} - const feeds = mapDefined(feedsRes, (row) => { - const feed = { - ...row, - viewer: viewer ? { like: row.viewerLike } : undefined, - } - return feedService.views.formatFeedGeneratorView(feed, profiles) - }) +type Context = { + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient +} - return { - encoding: 'application/json', - body: { - cursor: keyset.packFromResult(feedsRes), - feeds, - }, - } - }, - }) +type Params = QueryParams & { viewer: string | null } + +type Skeleton = { + feedUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts index 36e36b0100b..ad2de12dee0 100644 --- a/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getActorLikes.ts @@ -1,19 +1,15 @@ import { InvalidRequestError } from '@atproto/xrpc-server' +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getActorLikes' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { setRepoRev } from '../../../util' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { Database } from '../../../../db' -import { ActorService } from '../../../../services/actor' -import { GraphService } from '../../../../services/graph' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const getActorLikes = createPipeline( @@ -26,17 +22,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authOptionalVerifier, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.did - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const [result, repoRev] = await Promise.all([ - getActorLikes( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), + getActorLikes({ ...params, viewer }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) setRepoRev(res, repoRev) @@ -49,77 +38,78 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService, feedService } = ctx +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs const { actor, limit, cursor, viewer } = params - const { ref } = db.db.dynamic - const actorRes = await actorService.getActor(actor) - if (!actorRes) { + const [actorDid] = await ctx.hydrator.actor.getDids([actor]) + if (!actorDid || !viewer || viewer !== actorDid) { throw new InvalidRequestError('Profile not found') } - const actorDid = actorRes.did - if (!viewer || viewer !== actorDid) { - throw new InvalidRequestError('Profile not found') - } - - let feedItemsQb = feedService - .selectFeedItemQb() - .innerJoin('like', 'like.subject', 'feed_item.uri') - .where('like.creator', '=', actorDid) - - const keyset = new FeedKeyset(ref('like.sortAt'), ref('like.cid')) - - feedItemsQb = paginate(feedItemsQb, { + const likesRes = await ctx.dataplane.getActorLikes({ + actorDid, limit, cursor, - keyset, }) - const feedItems = await feedItemsQb.execute() + const postUris = likesRes.likes.map((l) => l.subject) - return { params, feedItems, cursor: keyset.packFromResult(feedItems) } + return { + postUris, + cursor: parseString(likesRes.cursor), + } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedPosts(skeleton.postUris, params.viewer) } -const noPostBlocks = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter( - (item) => !viewer || !state.bam.block([viewer, item.postAuthorDid]), - ) - return state +const noPostBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.postUris = skeleton.postUris.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.postUris, (uri) => + ctx.views.feedViewPost(uri, hydration), + ) + return { + feed, + cursor: skeleton.cursor, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; feedItems: FeedRow[]; cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState +type Skeleton = { + postUris: string[] + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts index 26b945f3ecd..fc871351cbd 100644 --- a/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts @@ -1,19 +1,19 @@ +import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed' -import { FeedKeyset } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { setRepoRev } from '../../../util' -import { Database } from '../../../../db' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { ActorService } from '../../../../services/actor' -import { GraphService } from '../../../../services/graph' import { createPipeline } from '../../../../pipeline' +import { + HydrationState, + Hydrator, + mergeStates, +} from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { Actor } from '../../../../hydration/actor' export default function (server: Server, ctx: AppContext) { const getAuthorFeed = createPipeline( @@ -25,19 +25,12 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getAuthorFeed({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth, res }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.type === 'access' ? auth.credentials.did : null const [result, repoRev] = await Promise.all([ - getAuthorFeed( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), + getAuthorFeed({ ...params, viewer }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) setRepoRev(res, repoRev) @@ -50,120 +43,104 @@ export default function (server: Server, ctx: AppContext) { }) } -export const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { cursor, limit, actor, filter, viewer } = params - const { db, actorService, feedService, graphService } = ctx - const { ref } = db.db.dynamic - - // maybe resolve did first - const actorRes = await actorService.getActor(actor) - if (!actorRes) { +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const [did] = await ctx.hydrator.actor.getDids([params.actor]) + if (!did) { throw new InvalidRequestError('Profile not found') } - const actorDid = actorRes.did - - // verify there is not a block between requester & subject - if (viewer !== null) { - const blocks = await graphService.getBlockState([[viewer, actorDid]]) - if (blocks.blocking([viewer, actorDid])) { - throw new InvalidRequestError( - `Requester has blocked actor: ${actor}`, - 'BlockedActor', - ) - } - if (blocks.blockedBy([viewer, actorDid])) { - throw new InvalidRequestError( - `Requester is blocked by actor: $${actor}`, - 'BlockedByActor', - ) - } - } - - // defaults to posts, reposts, and replies - let feedItemsQb = feedService - .selectFeedItemQb() - .where('originatorDid', '=', actorDid) - - if (filter === 'posts_with_media') { - feedItemsQb = feedItemsQb - // only your own posts - .where('type', '=', 'post') - // only posts with media - .whereExists((qb) => - qb - .selectFrom('post_embed_image') - .select('post_embed_image.postUri') - .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), - ) - } else if (filter === 'posts_no_replies') { - feedItemsQb = feedItemsQb.where((qb) => - qb.where('post.replyParent', 'is', null).orWhere('type', '=', 'repost'), - ) + const actors = await ctx.hydrator.actor.getActors([did]) + const actor = actors.get(did) + if (!actor || actor.takendown) { + throw new InvalidRequestError('Profile not found') } - - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - - feedItemsQb = paginate(feedItemsQb, { - limit, - cursor, - keyset, + const res = await ctx.dataplane.getAuthorFeed({ + actorDid: did, + limit: params.limit, + cursor: params.cursor, + noReplies: params.filter === 'posts_no_replies', + mediaOnly: params.filter === 'posts_with_media', }) - - const feedItems = await feedItemsQb.execute() - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + actor, + uris: res.items.map((item) => item.repost || item.uri), + cursor: parseString(res.cursor), } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + const [feedPostState, profileViewerState = {}] = await Promise.all([ + ctx.hydrator.hydrateFeedPosts(skeleton.uris, params.viewer), + params.viewer + ? ctx.hydrator.actor.getProfileViewerStates( + [skeleton.actor.did], + params.viewer, + ) + : undefined, + ]) + return mergeStates(feedPostState, profileViewerState) } -const noBlocksOrMutedReposts = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true +const noBlocksOrMutedReposts = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + const relationship = hydration.profileViewers?.get(skeleton.actor.did) + if (relationship?.blocking || relationship?.blockingByList) { + throw new InvalidRequestError( + `Requester has blocked actor: ${skeleton.actor.did}`, + 'BlockedActor', + ) + } + if (relationship?.blockedBy || relationship?.blockedByList) { + throw new InvalidRequestError( + `Requester is blocked by actor: ${skeleton.actor.did}`, + 'BlockedByActor', + ) + } + skeleton.uris = skeleton.uris.filter((uri) => { + const bam = ctx.views.feedItemBlocksAndMutes(uri, hydration) return ( - !state.bam.block([viewer, item.postAuthorDid]) && - (item.type === 'post' || !state.bam.mute([viewer, item.postAuthorDid])) + !bam.authorBlocked && + !bam.originatorBlocked && + !(bam.authorMuted && !bam.originatorMuted) ) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.uris, (uri) => + ctx.views.feedViewPost(uri, hydration), + ) + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - actorService: ActorService - feedService: FeedService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + actor: Actor + uris: string[] cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getFeed.ts b/packages/bsky/src/api/app/bsky/feed/getFeed.ts index a09258c3163..309e4400727 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeed.ts @@ -13,17 +13,17 @@ import { import { AtpAgent, AppBskyFeedGetFeedSkeleton } from '@atproto/api' import { QueryParams as GetFeedParams } from '../../../../lexicon/types/app/bsky/feed/getFeed' import { OutputSchema as SkeletonOutput } from '../../../../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { SkeletonFeedPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' -import { AlgoResponse } from '../../../../feed-gen/types' -import { Database } from '../../../../db' +import { AlgoResponse, AlgoResponseItem } from '../../../../feed-gen/types' import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { createPipeline } from '../../../../pipeline' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { mapDefined } from '@atproto/common' export default function (server: Server, ctx: AppContext) { const getFeed = createPipeline( @@ -35,18 +35,11 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeed({ auth: ctx.authOptionalVerifierAnyAudience, handler: async ({ params, auth, req }) => { - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) const viewer = auth.credentials.did const { timerSkele, timerHydr, ...result } = await getFeed( - { ...params, viewer }, - { - db, - feedService, - appCtx: ctx, - authorization: req.headers['authorization'], - }, + { ...params, viewer, authorization: req.headers['authorization'] }, + ctx, ) return { @@ -61,108 +54,99 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { + inputs: SkeletonFnInput, +): Promise => { + const { ctx, params } = inputs const timerSkele = new ServerTimer('skele').start() - const localAlgo = ctx.appCtx.algos[params.feed] - const feedParams: GetFeedParams = { - feed: params.feed, - limit: params.limit, - cursor: params.cursor, - } + const localAlgo = ctx.algos[params.feed] const { feedItems, cursor, ...passthrough } = localAlgo !== undefined - ? await localAlgo(ctx.appCtx, params, params.viewer) - : await skeletonFromFeedGen(ctx, feedParams) + ? await localAlgo(ctx, params, params.viewer) + : await skeletonFromFeedGen(ctx, params) return { - params, cursor, feedItems, timerSkele: timerSkele.stop(), + timerHydr: new ServerTimer('hydr').start(), passthrough, } } -const hydration = async (state: SkeletonState, ctx: Context) => { +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs const timerHydr = new ServerTimer('hydr').start() - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated, timerHydr: timerHydr.stop() } + const feedItemUris = skeleton.feedItems.map((item) => item.itemUri) + const hydration = await ctx.hydrator.hydrateFeedPosts( + feedItemUris, + params.viewer, + ) + skeleton.timerHydr = timerHydr.stop() + return hydration } -const noBlocksOrMutes = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true +const noBlocksOrMutes = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.feedItems = skeleton.feedItems.filter((item) => { + const bam = ctx.views.feedItemBlocksAndMutes(item.itemUri, hydration) return ( - !state.bam.block([viewer, item.postAuthorDid]) && - !state.bam.block([viewer, item.originatorDid]) && - !state.bam.mute([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.originatorDid]) + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted ) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, passthrough, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, params, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.feedItems, (item) => { + const view = ctx.views.feedViewPost(item.itemUri, hydration) + if (view?.post.uri !== item.postUri) { + return undefined + } else { + return view + } + }).slice(0, params.limit) return { feed, - cursor, - timerSkele: state.timerSkele, - timerHydr: state.timerHydr, - ...passthrough, + cursor: skeleton.cursor, + timerSkele: skeleton.timerSkele, + timerHydr: skeleton.timerHydr, + ...skeleton.passthrough, } } -type Context = { - db: Database - feedService: FeedService - appCtx: AppContext - authorization?: string -} +type Context = AppContext -type Params = GetFeedParams & { viewer: string | null } +type Params = GetFeedParams & { viewer: string | null; authorization?: string } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + feedItems: AlgoResponseItem[] passthrough: Record // pass through additional items in feedgen response cursor?: string timerSkele: ServerTimer + timerHydr: ServerTimer } -type HydrationState = SkeletonState & - FeedHydrationState & { feedItems: FeedRow[]; timerHydr: ServerTimer } - const skeletonFromFeedGen = async ( ctx: Context, - params: GetFeedParams, + params: Params, ): Promise => { - const { db, appCtx, authorization } = ctx const { feed } = params - // Resolve and fetch feed skeleton - const found = await db.db - .selectFrom('feed_generator') - .where('uri', '=', feed) - .select('feedDid') - .executeTakeFirst() - if (!found) { + const found = await ctx.hydrator.feed.getFeedGens([feed], true) + const feedDid = await found.get(feed)?.record.did + if (!feedDid) { throw new InvalidRequestError('could not find feed') } - const feedDid = found.feedDid let resolved: DidDocument | null try { - resolved = await appCtx.idResolver.did.resolve(feedDid) + resolved = await ctx.idResolver.did.resolve(feedDid) } catch (err) { if (err instanceof PoorlyFormattedDidDocumentError) { throw new InvalidRequestError(`invalid did document: ${feedDid}`) @@ -185,12 +169,19 @@ const skeletonFromFeedGen = async ( let skeleton: SkeletonOutput try { // @TODO currently passthrough auth headers from pds - const headers: Record = authorization - ? { authorization: authorization } + const headers: Record = params.authorization + ? { authorization: params.authorization } : {} - const result = await agent.api.app.bsky.feed.getFeedSkeleton(params, { - headers, - }) + const result = await agent.api.app.bsky.feed.getFeedSkeleton( + { + feed: params.feed, + limit: params.limit, + cursor: params.cursor, + }, + { + headers, + }, + ) skeleton = result.data } catch (err) { if (err instanceof AppBskyFeedGetFeedSkeleton.UnknownFeedError) { @@ -211,33 +202,11 @@ const skeletonFromFeedGen = async ( } const { feed: feedSkele, ...skele } = skeleton - const feedItems = await skeletonToFeedItems( - feedSkele.slice(0, params.limit), - ctx, - ) + const feedItems = feedSkele.map((item) => ({ + itemUri: + typeof item.reason?.repost === 'string' ? item.reason.repost : item.post, + postUri: item.post, + })) return { ...skele, feedItems } } - -const skeletonToFeedItems = async ( - skeleton: SkeletonFeedPost[], - ctx: Context, -): Promise => { - const { feedService } = ctx - const feedItemUris = skeleton.map(getSkeleFeedItemUri) - const feedItemsRaw = await feedService.getFeedItems(feedItemUris) - const results: FeedRow[] = [] - for (const skeleItem of skeleton) { - const feedItem = feedItemsRaw[getSkeleFeedItemUri(skeleItem)] - if (feedItem && feedItem.postUri === skeleItem.post) { - results.push(feedItem) - } - } - return results -} - -const getSkeleFeedItemUri = (item: SkeletonFeedPost) => { - return typeof item.reason?.repost === 'string' - ? item.reason.repost - : item.post -} diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts index 14a5688db0d..6e2f4e57b2a 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerator.ts @@ -14,17 +14,13 @@ export default function (server: Server, ctx: AppContext) { const { feed } = params const viewer = auth.credentials.did - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - - const got = await feedService.getFeedGeneratorInfos([feed], viewer) - const feedInfo = got[feed] + const hydration = await ctx.hydrator.hydrateFeedGens([feed], viewer) + const feedInfo = hydration.feedgens?.get(feed) if (!feedInfo) { throw new InvalidRequestError('could not find feed') } - const feedDid = feedInfo.feedDid + const feedDid = feedInfo.record.did let resolved: DidDocument | null try { resolved = await ctx.idResolver.did.resolve(feedDid) @@ -47,14 +43,7 @@ export default function (server: Server, ctx: AppContext) { ) } - const profiles = await actorService.views.profilesBasic( - [feedInfo.creator], - viewer, - ) - const feedView = feedService.views.formatFeedGeneratorView( - feedInfo, - profiles, - ) + const feedView = ctx.views.feedGenerator(feed, hydration) if (!feedView) { throw new InvalidRequestError('could not find feed') } diff --git a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts index 7b571ab09f6..c2b05bb908b 100644 --- a/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts +++ b/packages/bsky/src/api/app/bsky/feed/getFeedGenerators.ts @@ -1,10 +1,10 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getFeedGenerators' import AppContext from '../../../../context' -import { FeedGenInfo, FeedService } from '../../../../services/feed' import { createPipeline, noRules } from '../../../../pipeline' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { Database } from '../../../../db' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getFeedGenerators = createPipeline( @@ -16,17 +16,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getFeedGenerators({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { feeds } = params const viewer = auth.credentials.did - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - - const view = await getFeedGenerators( - { feeds, viewer }, - { db, feedService, actorService }, - ) - + const view = await getFeedGenerators({ ...params, viewer }, ctx) return { encoding: 'application/json', body: view, @@ -35,46 +26,42 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async (params: Params, ctx: Context) => { - const { feedService } = ctx - const genInfos = await feedService.getFeedGeneratorInfos( - params.feeds, - params.viewer, - ) +const skeleton = async (inputs: { params: Params }): Promise => { return { - params, - generators: Object.values(genInfos), + feedUris: inputs.params.feeds, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const profiles = await actorService.views.profilesBasic( - state.generators.map((gen) => gen.creator), - state.params.viewer, - ) - return { - ...state, - profiles, - } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateFeedGens(skeleton.feedUris, params.viewer) } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const feeds = mapDefined(state.generators, (gen) => - feedService.views.formatFeedGeneratorView(gen, state.profiles), +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feeds = mapDefined(skeleton.feedUris, (uri) => + ctx.views.feedGenerator(uri, hydration), ) - return { feeds } + return { + feeds, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + hydrator: Hydrator + views: Views } -type Params = { viewer: string | null; feeds: string[] } +type Params = QueryParams & { viewer: string | null } -type SkeletonState = { params: Params; generators: FeedGenInfo[] } - -type HydrationState = SkeletonState & { profiles: ActorInfoMap } +type Skeleton = { + feedUris: string[] +} diff --git a/packages/bsky/src/api/app/bsky/feed/getLikes.ts b/packages/bsky/src/api/app/bsky/feed/getLikes.ts index 893617f6bb0..133230dc88e 100644 --- a/packages/bsky/src/api/app/bsky/feed/getLikes.ts +++ b/packages/bsky/src/api/app/bsky/feed/getLikes.ts @@ -1,29 +1,21 @@ import { mapDefined } from '@atproto/common' +import { normalizeDatetimeAlways } from '@atproto/syntax' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getLikes' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { Actor } from '../../../../db/tables/actor' -import { Database } from '../../../../db' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const getLikes = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getLikes({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.did - - const result = await getLikes( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getLikes({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -33,95 +25,83 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { uri, cid, limit, cursor } = params - const { ref } = db.db.dynamic - - let builder = db.db - .selectFrom('like') - .where('like.subject', '=', uri) - .innerJoin('actor as creator', 'creator.did', 'like.creator') - .where(notSoftDeletedClause(ref('creator'))) - .selectAll('creator') - .select([ - 'like.cid as cid', - 'like.createdAt as createdAt', - 'like.indexedAt as indexedAt', - 'like.sortAt as sortAt', - ]) - - if (cid) { - builder = builder.where('like.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const likesRes = await ctx.hydrator.dataplane.getLikesBySubject({ + subject: { uri: params.uri, cid: params.cid }, + cursor: params.cursor, + limit: params.limit, }) - - const likes = await builder.execute() - - return { params, likes, cursor: keyset.packFromResult(likes) } + return { + likes: likesRes.uris, + cursor: parseString(likesRes.cursor), + } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, likes } = state - const { viewer } = params - const [actors, bam] = await Promise.all([ - actorService.views.profiles(likes, viewer), - graphService.getBlockAndMuteState( - viewer ? likes.map((like) => [viewer, like.did]) : [], - ), - ]) - return { ...state, bam, actors } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateLikes(skeleton.likes, params.viewer) } -const noBlocks = (state: HydrationState) => { - const { viewer } = state.params - if (!viewer) return state - state.likes = state.likes.filter( - (item) => !state.bam.block([viewer, item.did]), - ) - return state +const noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.likes = skeleton.likes.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -const presentation = (state: HydrationState) => { - const { params, likes, actors, cursor } = state - const { uri, cid } = params - const likesView = mapDefined(likes, (like) => - actors[like.did] - ? { - createdAt: like.createdAt, - indexedAt: like.indexedAt, - actor: actors[like.did], - } - : undefined, - ) - return { likes: likesView, cursor, uri, cid } +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = inputs + const likeViews = mapDefined(skeleton.likes, (uri) => { + const like = hydration.likes?.get(uri) + if (!like || !like.indexedAt || !like.record) { + return + } + const creatorDid = creatorFromUri(uri) + const actor = ctx.views.profile(creatorDid, hydration) + if (!actor) { + return + } + return { + actor, + createdAt: normalizeDatetimeAlways(like.record.createdAt), + indexedAt: like.indexedAt.toISOString(), + } + }) + return { + likes: likeViews, + cursor: skeleton.cursor, + uri: params.uri, + cid: params.cid, + } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - likes: (Actor & { createdAt: string })[] +type Skeleton = { + likes: string[] cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap -} diff --git a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts index fd3f0360ef3..94f3c62e4ed 100644 --- a/packages/bsky/src/api/app/bsky/feed/getListFeed.ts +++ b/packages/bsky/src/api/app/bsky/feed/getListFeed.ts @@ -1,18 +1,13 @@ import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getListFeed' -import { FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' import { setRepoRev } from '../../../util' -import { Database } from '../../../../db' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { ActorService } from '../../../../services/actor' -import { GraphService } from '../../../../services/graph' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { mapDefined } from '@atproto/common' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { const getListFeed = createPipeline( @@ -25,17 +20,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authOptionalVerifier, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.did - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const feedService = ctx.services.feed(db) - const graphService = ctx.services.graph(db) const [result, repoRev] = await Promise.all([ - getListFeed( - { ...params, viewer }, - { db, actorService, feedService, graphService }, - ), - actorService.getRepoRev(viewer), + getListFeed({ ...params, viewer }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) setRepoRev(res, repoRev) @@ -48,80 +36,70 @@ export default function (server: Server, ctx: AppContext) { }) } -export const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { list, cursor, limit } = params - const { db } = ctx - const { ref } = db.db.dynamic - - const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) - const sortFrom = keyset.unpack(cursor)?.primary - - let builder = ctx.feedService - .selectPostQb() - .innerJoin('list_item', 'list_item.subjectDid', 'post.creator') - .where('list_item.listUri', '=', list) - .where('post.sortAt', '>', getFeedDateThreshold(sortFrom, 3)) - - builder = paginate(builder, { - limit, - cursor, - keyset, - tryIndex: true, +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const res = await ctx.dataplane.getListFeed({ + listUri: params.list, + limit: params.limit, + cursor: params.cursor, }) - const feedItems = await builder.execute() - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + uris: res.uris, + cursor: parseString(res.cursor), } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateFeedPosts(skeleton.uris, params.viewer) } -const noBlocksOrMutes = (state: HydrationState) => { - const { viewer } = state.params - if (!viewer) return state - state.feedItems = state.feedItems.filter( - (item) => - !state.bam.block([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.postAuthorDid]), - ) - return state +const noBlocksOrMutes = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + skeleton.uris = skeleton.uris.filter((uri) => { + const bam = ctx.views.feedItemBlocksAndMutes(uri, hydration) + return ( + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted + ) + }) + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.uris, (uri) => + ctx.views.feedViewPost(uri, hydration), + ) + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - actorService: ActorService - feedService: FeedService - graphService: GraphService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + uris: string[] cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts index 873dd311ba0..d1ba6619a36 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPostThread.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPostThread.ts @@ -1,27 +1,19 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { AtUri } from '@atproto/syntax' import { Server } from '../../../../lexicon' -import { - BlockedPost, - NotFoundPost, - ThreadViewPost, - isNotFoundPost, -} from '../../../../lexicon/types/app/bsky/feed/defs' +import { isNotFoundPost } from '../../../../lexicon/types/app/bsky/feed/defs' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPostThread' import AppContext from '../../../../context' -import { - FeedService, - FeedRow, - FeedHydrationState, -} from '../../../../services/feed' -import { - getAncestorsAndSelfQb, - getDescendentsQb, -} from '../../../../services/util/post' -import { Database } from '../../../../db' import { setRepoRev } from '../../../util' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { createPipeline, noRules } from '../../../../pipeline' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' export default function (server: Server, ctx: AppContext) { const getPostThread = createPipeline( @@ -34,13 +26,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth, res }) => { const viewer = 'did' in auth.credentials ? auth.credentials.did : null - const db = ctx.db.getReplica('thread') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) const [result, repoRev] = await Promise.allSettled([ - getPostThread({ ...params, viewer }, { db, feedService, actorService }), - actorService.getRepoRev(viewer), + getPostThread({ ...params, viewer }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) if (repoRev.status === 'fulfilled') { @@ -58,40 +47,34 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async (params: Params, ctx: Context) => { - const threadData = await getThreadData(params, ctx) - if (!threadData) { - throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const res = await ctx.dataplane.getThread({ + postUri: params.uri, + above: params.parentHeight, + below: params.depth, + }) + return { + anchor: params.uri, + uris: res.uris, } - return { params, threadData } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { feedService } = ctx - const { - threadData, - params: { viewer }, - } = state - const relevant = getRelevantIds(threadData) - const hydrated = await feedService.feedHydration({ ...relevant, viewer }) - return { ...state, ...hydrated } +const hydration = async ( + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateThreadPosts(skeleton.uris, params.viewer) } -const presentation = (state: HydrationState, ctx: Context) => { - const { params, profiles } = state - const { actorService } = ctx - const actors = actorService.views.profileBasicPresentation( - Object.keys(profiles), - state, - params.viewer, - ) - const thread = composeThread( - state.threadData, - actors, - state, - ctx, - params.viewer, - ) +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, params, skeleton, hydration } = inputs + const thread = ctx.views.thread(skeleton, hydration, { + height: params.parentHeight, + depth: params.depth, + }) if (isNotFoundPost(thread)) { // @TODO technically this could be returned as a NotFoundPost based on lexicon throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound') @@ -99,233 +82,232 @@ const presentation = (state: HydrationState, ctx: Context) => { return { thread } } -const composeThread = ( - threadData: PostThread, - actors: ActorInfoMap, - state: HydrationState, - ctx: Context, - viewer: string | null, -) => { - const { feedService } = ctx - const { posts, threadgates, embeds, blocks, labels, lists } = state +// @TODO tidy +// const composeThread = ( +// threadData: PostThread, +// actors: ActorInfoMap, +// state: HydrationState, +// ctx: Context, +// viewer: string | null, +// ) => { +// const { feedService } = ctx +// const { posts, threadgates, embeds, blocks, labels, lists } = state - const post = feedService.views.formatPostView( - threadData.post.postUri, - actors, - posts, - threadgates, - embeds, - labels, - lists, - viewer, - ) +// const post = feedService.views.formatPostView( +// threadData.post.postUri, +// actors, +// posts, +// threadgates, +// embeds, +// labels, +// lists, +// viewer, +// ) - // replies that are invalid due to reply-gating: - // a. may appear as the anchor post, but without any parent or replies. - // b. may not appear anywhere else in the thread. - const isAnchorPost = state.threadData.post.uri === threadData.post.postUri - const info = posts[threadData.post.postUri] - // @TODO re-enable invalidReplyRoot check - // const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate - const badReply = !!info?.violatesThreadGate - const omitBadReply = !isAnchorPost && badReply +// // replies that are invalid due to reply-gating: +// // a. may appear as the anchor post, but without any parent or replies. +// // b. may not appear anywhere else in the thread. +// const isAnchorPost = state.threadData.post.uri === threadData.post.postUri +// const info = posts[threadData.post.postUri] +// // @TODO re-enable invalidReplyRoot check +// // const badReply = !!info?.invalidReplyRoot || !!info?.violatesThreadGate +// const badReply = !!info?.violatesThreadGate +// const omitBadReply = !isAnchorPost && badReply - if (!post || blocks[post.uri]?.reply || omitBadReply) { - return { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.post.postUri, - notFound: true, - } - } +// if (!post || blocks[post.uri]?.reply || omitBadReply) { +// return { +// $type: 'app.bsky.feed.defs#notFoundPost', +// uri: threadData.post.postUri, +// notFound: true, +// } +// } - if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { - return { - $type: 'app.bsky.feed.defs#blockedPost', - uri: threadData.post.postUri, - blocked: true, - author: { - did: post.author.did, - viewer: post.author.viewer - ? { - blockedBy: post.author.viewer?.blockedBy, - blocking: post.author.viewer?.blocking, - } - : undefined, - }, - } - } +// if (post.author.viewer?.blocking || post.author.viewer?.blockedBy) { +// return { +// $type: 'app.bsky.feed.defs#blockedPost', +// uri: threadData.post.postUri, +// blocked: true, +// author: { +// did: post.author.did, +// viewer: post.author.viewer +// ? { +// blockedBy: post.author.viewer?.blockedBy, +// blocking: post.author.viewer?.blocking, +// } +// : undefined, +// }, +// } +// } - let parent - if (threadData.parent && !badReply) { - if (threadData.parent instanceof ParentNotFoundError) { - parent = { - $type: 'app.bsky.feed.defs#notFoundPost', - uri: threadData.parent.uri, - notFound: true, - } - } else { - parent = composeThread(threadData.parent, actors, state, ctx, viewer) - } - } +// let parent +// if (threadData.parent && !badReply) { +// if (threadData.parent instanceof ParentNotFoundError) { +// parent = { +// $type: 'app.bsky.feed.defs#notFoundPost', +// uri: threadData.parent.uri, +// notFound: true, +// } +// } else { +// parent = composeThread(threadData.parent, actors, state, ctx, viewer) +// } +// } - let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined - if (threadData.replies && !badReply) { - replies = threadData.replies.flatMap((reply) => { - const thread = composeThread(reply, actors, state, ctx, viewer) - // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. - const skip = [] - return isNotFoundPost(thread) ? skip : thread - }) - } +// let replies: (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined +// if (threadData.replies && !badReply) { +// replies = threadData.replies.flatMap((reply) => { +// const thread = composeThread(reply, actors, state, ctx, viewer) +// // e.g. don't bother including #postNotFound reply placeholders for takedowns. either way matches api contract. +// const skip = [] +// return isNotFoundPost(thread) ? skip : thread +// }) +// } - return { - $type: 'app.bsky.feed.defs#threadViewPost', - post, - parent, - replies, - } -} +// return { +// $type: 'app.bsky.feed.defs#threadViewPost', +// post, +// parent, +// replies, +// } +// } -const getRelevantIds = ( - thread: PostThread, -): { dids: Set; uris: Set } => { - const dids = new Set() - const uris = new Set() - if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) { - const fromParent = getRelevantIds(thread.parent) - fromParent.dids.forEach((did) => dids.add(did)) - fromParent.uris.forEach((uri) => uris.add(uri)) - } - if (thread.replies) { - for (const reply of thread.replies) { - const fromChild = getRelevantIds(reply) - fromChild.dids.forEach((did) => dids.add(did)) - fromChild.uris.forEach((uri) => uris.add(uri)) - } - } - dids.add(thread.post.postAuthorDid) - uris.add(thread.post.postUri) - if (thread.post.replyRoot) { - // ensure root is included for checking interactions - uris.add(thread.post.replyRoot) - dids.add(new AtUri(thread.post.replyRoot).hostname) - } - return { dids, uris } -} +// const getRelevantIds = ( +// thread: PostThread, +// ): { dids: Set; uris: Set } => { +// const dids = new Set() +// const uris = new Set() +// if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) { +// const fromParent = getRelevantIds(thread.parent) +// fromParent.dids.forEach((did) => dids.add(did)) +// fromParent.uris.forEach((uri) => uris.add(uri)) +// } +// if (thread.replies) { +// for (const reply of thread.replies) { +// const fromChild = getRelevantIds(reply) +// fromChild.dids.forEach((did) => dids.add(did)) +// fromChild.uris.forEach((uri) => uris.add(uri)) +// } +// } +// dids.add(thread.post.postAuthorDid) +// uris.add(thread.post.postUri) +// if (thread.post.replyRoot) { +// // ensure root is included for checking interactions +// uris.add(thread.post.replyRoot) +// dids.add(new AtUri(thread.post.replyRoot).hostname) +// } +// return { dids, uris } +// } -const getThreadData = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, feedService } = ctx - const { uri, depth, parentHeight } = params +// const getThreadData = async ( +// params: Params, +// ctx: Context, +// ): Promise => { +// const { db, feedService } = ctx +// const { uri, depth, parentHeight } = params - const [parents, children] = await Promise.all([ - getAncestorsAndSelfQb(db.db, { uri, parentHeight }) - .selectFrom('ancestor') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'ancestor.uri', - ) - .selectAll('post') - .execute(), - getDescendentsQb(db.db, { uri, depth }) - .selectFrom('descendent') - .innerJoin( - feedService.selectPostQb().as('post'), - 'post.uri', - 'descendent.uri', - ) - .selectAll('post') - .orderBy('sortAt', 'desc') - .execute(), - ]) - // prevent self-referential loops - const includedPosts = new Set([uri]) - const parentsByUri = parents.reduce((acc, post) => { - return Object.assign(acc, { [post.uri]: post }) - }, {} as Record) - const childrenByParentUri = children.reduce((acc, child) => { - if (!child.replyParent) return acc - if (includedPosts.has(child.uri)) return acc - includedPosts.add(child.uri) - acc[child.replyParent] ??= [] - acc[child.replyParent].push(child) - return acc - }, {} as Record) - const post = parentsByUri[uri] - if (!post) return null - return { - post, - parent: post.replyParent - ? getParentData( - parentsByUri, - includedPosts, - post.replyParent, - parentHeight, - ) - : undefined, - replies: getChildrenData(childrenByParentUri, uri, depth), - } -} +// const [parents, children] = await Promise.all([ +// getAncestorsAndSelfQb(db.db, { uri, parentHeight }) +// .selectFrom('ancestor') +// .innerJoin( +// feedService.selectPostQb().as('post'), +// 'post.uri', +// 'ancestor.uri', +// ) +// .selectAll('post') +// .execute(), +// getDescendentsQb(db.db, { uri, depth }) +// .selectFrom('descendent') +// .innerJoin( +// feedService.selectPostQb().as('post'), +// 'post.uri', +// 'descendent.uri', +// ) +// .selectAll('post') +// .orderBy('sortAt', 'desc') +// .execute(), +// ]) +// // prevent self-referential loops +// const includedPosts = new Set([uri]) +// const parentsByUri = parents.reduce((acc, post) => { +// return Object.assign(acc, { [post.uri]: post }) +// }, {} as Record) +// const childrenByParentUri = children.reduce((acc, child) => { +// if (!child.replyParent) return acc +// if (includedPosts.has(child.uri)) return acc +// includedPosts.add(child.uri) +// acc[child.replyParent] ??= [] +// acc[child.replyParent].push(child) +// return acc +// }, {} as Record) +// const post = parentsByUri[uri] +// if (!post) return null +// return { +// post, +// parent: post.replyParent +// ? getParentData( +// parentsByUri, +// includedPosts, +// post.replyParent, +// parentHeight, +// ) +// : undefined, +// replies: getChildrenData(childrenByParentUri, uri, depth), +// } +// } -const getParentData = ( - postsByUri: Record, - includedPosts: Set, - uri: string, - depth: number, -): PostThread | ParentNotFoundError | undefined => { - if (depth < 1) return undefined - if (includedPosts.has(uri)) return undefined - includedPosts.add(uri) - const post = postsByUri[uri] - if (!post) return new ParentNotFoundError(uri) - return { - post, - parent: post.replyParent - ? getParentData(postsByUri, includedPosts, post.replyParent, depth - 1) - : undefined, - replies: [], - } -} +// const getParentData = ( +// postsByUri: Record, +// includedPosts: Set, +// uri: string, +// depth: number, +// ): PostThread | ParentNotFoundError | undefined => { +// if (depth < 1) return undefined +// if (includedPosts.has(uri)) return undefined +// includedPosts.add(uri) +// const post = postsByUri[uri] +// if (!post) return new ParentNotFoundError(uri) +// return { +// post, +// parent: post.replyParent +// ? getParentData(postsByUri, includedPosts, post.replyParent, depth - 1) +// : undefined, +// replies: [], +// } +// } -const getChildrenData = ( - childrenByParentUri: Record, - uri: string, - depth: number, -): PostThread[] | undefined => { - if (depth === 0) return undefined - const children = childrenByParentUri[uri] ?? [] - return children.map((row) => ({ - post: row, - replies: getChildrenData(childrenByParentUri, row.postUri, depth - 1), - })) -} +// const getChildrenData = ( +// childrenByParentUri: Record, +// uri: string, +// depth: number, +// ): PostThread[] | undefined => { +// if (depth === 0) return undefined +// const children = childrenByParentUri[uri] ?? [] +// return children.map((row) => ({ +// post: row, +// replies: getChildrenData(childrenByParentUri, row.postUri, depth - 1), +// })) +// } -class ParentNotFoundError extends Error { - constructor(public uri: string) { - super(`Parent not found: ${uri}`) - } -} +// class ParentNotFoundError extends Error { +// constructor(public uri: string) { +// super(`Parent not found: ${uri}`) +// } +// } -type PostThread = { - post: FeedRow - parent?: PostThread | ParentNotFoundError - replies?: PostThread[] -} +// type PostThread = { +// post: FeedRow +// parent?: PostThread | ParentNotFoundError +// replies?: PostThread[] +// } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - threadData: PostThread +type Skeleton = { + anchor: string + uris: string[] } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getPosts.ts b/packages/bsky/src/api/app/bsky/feed/getPosts.ts index 5ec4807accb..7c11cb78f74 100644 --- a/packages/bsky/src/api/app/bsky/feed/getPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/getPosts.ts @@ -1,30 +1,19 @@ -import { dedupeStrs } from '@atproto/common' +import { dedupeStrs, mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPosts' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' import { createPipeline } from '../../../../pipeline' -import { ActorService } from '../../../../services/actor' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const getPosts = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.feed.getPosts({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.did - - const results = await getPosts( - { ...params, viewer }, - { db, feedService, actorService }, - ) + const results = await getPosts({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -34,68 +23,52 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async (params: Params, ctx: Context) => { - const deduped = dedupeStrs(params.uris) - const feedItems = await ctx.feedService.postUrisToFeedItems(deduped) - return { params, feedItems } +const skeleton = async (inputs: { params: Params }) => { + return { posts: dedupeStrs(inputs.params.uris) } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydratePosts(skeleton.posts, params.viewer) } -const noBlocks = (state: HydrationState) => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true - return !state.bam.block([viewer, item.postAuthorDid]) +const noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.posts = skeleton.posts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService, actorService } = ctx - const { feedItems, profiles, params } = state - const SKIP = [] - const actors = actorService.views.profileBasicPresentation( - Object.keys(profiles), - state, - params.viewer, +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const posts = mapDefined(skeleton.posts, (uri) => + ctx.views.post(uri, hydration), ) - const postViews = feedItems.flatMap((item) => { - const postView = feedService.views.formatPostView( - item.postUri, - actors, - state.posts, - state.threadgates, - state.embeds, - state.labels, - state.lists, - params.viewer, - ) - return postView ?? SKIP - }) - return { posts: postViews } + return { posts } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + posts: string[] } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts index 5ca5c452b63..61d6f433b4d 100644 --- a/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts +++ b/packages/bsky/src/api/app/bsky/feed/getRepostedBy.ts @@ -1,14 +1,12 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getRepostedBy' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' -import { Database } from '../../../../db' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' -import { Actor } from '../../../../db/tables/actor' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const getRepostedBy = createPipeline( @@ -20,15 +18,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getRepostedBy({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = auth.credentials.did - - const result = await getRepostedBy( - { ...params, viewer }, - { db, actorService, graphService }, - ) + const result = await getRepostedBy({ ...params, viewer }, ctx) return { encoding: 'application/json', @@ -38,81 +29,75 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db } = ctx - const { limit, cursor, uri, cid } = params - const { ref } = db.db.dynamic - - let builder = db.db - .selectFrom('repost') - .where('repost.subject', '=', uri) - .innerJoin('actor as creator', 'creator.did', 'repost.creator') - .where(notSoftDeletedClause(ref('creator'))) - .selectAll('creator') - .select(['repost.cid as cid', 'repost.sortAt as sortAt']) - - if (cid) { - builder = builder.where('repost.subjectCid', '=', cid) - } - - const keyset = new TimeCidKeyset(ref('repost.sortAt'), ref('repost.cid')) - builder = paginate(builder, { - limit, - cursor, - keyset, +const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const res = await ctx.hydrator.dataplane.getRepostsBySubject({ + subjectUri: params.uri, + cursor: params.cursor, + limit: params.limit, }) - - const repostedBy = await builder.execute() - return { params, repostedBy, cursor: keyset.packFromResult(repostedBy) } + return { + reposts: res.uris, + cursor: parseString(res.cursor), + } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, repostedBy } = state - const { viewer } = params - const [actors, bam] = await Promise.all([ - actorService.views.profiles(repostedBy, viewer), - graphService.getBlockAndMuteState( - viewer ? repostedBy.map((item) => [viewer, item.did]) : [], - ), - ]) - return { ...state, bam, actors } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}) => { + const { ctx, params, skeleton } = inputs + return await ctx.hydrator.hydrateReposts(skeleton.reposts, params.viewer) } -const noBlocks = (state: HydrationState) => { - const { viewer } = state.params - if (!viewer) return state - state.repostedBy = state.repostedBy.filter( - (item) => !state.bam.block([viewer, item.did]), - ) - return state +const noBlocks = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + skeleton.reposts = skeleton.reposts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) + }) + return skeleton } -const presentation = (state: HydrationState) => { - const { params, repostedBy, actors, cursor } = state - const { uri, cid } = params - const repostedByView = mapDefined(repostedBy, (item) => actors[item.did]) - return { repostedBy: repostedByView, cursor, uri, cid } +const presentation = (inputs: { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, params, skeleton, hydration } = inputs + const repostViews = mapDefined(skeleton.reposts, (uri) => { + const repost = hydration.reposts?.get(uri) + if (!repost?.record) { + return + } + const creatorDid = creatorFromUri(uri) + return ctx.views.profile(creatorDid, hydration) + }) + return { + repostedBy: repostViews, + cursor: skeleton.cursor, + uri: params.uri, + cid: params.cid, + } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - repostedBy: Actor[] +type Skeleton = { + reposts: string[] cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap -} diff --git a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts index 35fac829039..298eada8aa1 100644 --- a/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts +++ b/packages/bsky/src/api/app/bsky/feed/getSuggestedFeeds.ts @@ -1,37 +1,30 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import AppContext from '../../../../context' +import { parseString } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.feed.getSuggestedFeeds({ auth: ctx.authOptionalVerifier, - handler: async ({ auth }) => { + handler: async ({ params, auth }) => { const viewer = auth.credentials.did - const db = ctx.db.getReplica() - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - const feedsRes = await db.db - .selectFrom('suggested_feed') - .orderBy('suggested_feed.order', 'asc') - .selectAll() - .execute() - const genInfos = await feedService.getFeedGeneratorInfos( - feedsRes.map((r) => r.uri), - viewer, - ) - const genList = feedsRes.map((r) => genInfos[r.uri]).filter(Boolean) - const creators = genList.map((gen) => gen.creator) - const profiles = await actorService.views.profilesBasic(creators, viewer) - - const feedViews = mapDefined(genList, (gen) => - feedService.views.formatFeedGeneratorView(gen, profiles), + const suggestedRes = await ctx.dataplane.getSuggestedFeeds({ + actorDid: viewer ?? undefined, + limit: params.limit, + cursor: params.cursor, + }) + const uris = suggestedRes.uris + const hydration = await ctx.hydrator.hydrateFeedGens(uris, viewer) + const feedViews = mapDefined(uris, (uri) => + ctx.views.feedGenerator(uri, hydration), ) return { encoding: 'application/json', body: { feeds: feedViews, + cursor: parseString(suggestedRes.cursor), }, } }, diff --git a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts index 18cc5c2629a..d7bdfa9399f 100644 --- a/packages/bsky/src/api/app/bsky/feed/getTimeline.ts +++ b/packages/bsky/src/api/app/bsky/feed/getTimeline.ts @@ -1,17 +1,13 @@ -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { FeedAlgorithm, FeedKeyset, getFeedDateThreshold } from '../util/feed' -import { paginate } from '../../../../db/pagination' import AppContext from '../../../../context' -import { Database } from '../../../../db' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getTimeline' import { setRepoRev } from '../../../util' -import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' import { createPipeline } from '../../../../pipeline' +import { HydrationState, Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { mapDefined } from '@atproto/common' export default function (server: Server, ctx: AppContext) { const getTimeline = createPipeline( @@ -24,13 +20,10 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authVerifier, handler: async ({ params, auth, res }) => { const viewer = auth.credentials.did - const db = ctx.db.getReplica('timeline') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) const [result, repoRev] = await Promise.all([ - getTimeline({ ...params, viewer }, { db, feedService }), - actorService.getRepoRev(viewer), + getTimeline({ ...params, viewer }, ctx), + ctx.hydrator.actor.getRepoRevSafe(viewer), ]) setRepoRev(res, repoRev) @@ -43,124 +36,70 @@ export default function (server: Server, ctx: AppContext) { }) } -export const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { cursor, limit, algorithm, viewer } = params - const { db } = ctx - const { ref } = db.db.dynamic - - if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) { - throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`) - } - - const keyset = new FeedKeyset(ref('feed_item.sortAt'), ref('feed_item.cid')) - const sortFrom = keyset.unpack(cursor)?.primary - - let followQb = db.db - .selectFrom('feed_item') - .innerJoin('follow', 'follow.subjectDid', 'feed_item.originatorDid') - .where('follow.creator', '=', viewer) - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 2)) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - - followQb = paginate(followQb, { - limit, - cursor, - keyset, - tryIndex: true, - }) - - let selfQb = db.db - .selectFrom('feed_item') - .innerJoin('post', 'post.uri', 'feed_item.postUri') - .where('feed_item.originatorDid', '=', viewer) - .where('feed_item.sortAt', '>', getFeedDateThreshold(sortFrom, 2)) - .selectAll('feed_item') - .select([ - 'post.replyRoot', - 'post.replyParent', - 'post.creator as postAuthorDid', - ]) - - selfQb = paginate(selfQb, { - limit: Math.min(limit, 10), - cursor, - keyset, - tryIndex: true, +export const skeleton = async (inputs: { + ctx: Context + params: Params +}): Promise => { + const { ctx, params } = inputs + const res = await ctx.dataplane.getTimeline({ + actorDid: params.viewer, + limit: params.limit, + cursor: params.cursor, }) - - const [followRes, selfRes] = await Promise.all([ - followQb.execute(), - selfQb.execute(), - ]) - - const feedItems: FeedRow[] = [...followRes, ...selfRes] - .sort((a, b) => { - if (a.sortAt > b.sortAt) return -1 - if (a.sortAt < b.sortAt) return 1 - return a.cid > b.cid ? -1 : 1 - }) - .slice(0, limit) - return { - params, - feedItems, - cursor: keyset.packFromResult(feedItems), + uris: res.items.map((item) => item.repost || item.uri), + cursor: parseString(res.cursor), } } -const hydration = async ( - state: SkeletonState, - ctx: Context, -): Promise => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } +const hydration = async (inputs: { + ctx: Context + params: Params + skeleton: Skeleton +}): Promise => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydrateFeedPosts(skeleton.uris, params.viewer) } -const noBlocksOrMutes = (state: HydrationState): HydrationState => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter( - (item) => - !state.bam.block([viewer, item.postAuthorDid]) && - !state.bam.block([viewer, item.originatorDid]) && - !state.bam.mute([viewer, item.postAuthorDid]) && - !state.bam.mute([viewer, item.originatorDid]), - ) - return state +const noBlocksOrMutes = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}): Skeleton => { + const { ctx, skeleton, hydration } = inputs + skeleton.uris = skeleton.uris.filter((uri) => { + const bam = ctx.views.feedItemBlocksAndMutes(uri, hydration) + return ( + !bam.authorBlocked && + !bam.authorMuted && + !bam.originatorBlocked && + !bam.originatorMuted + ) + }) + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService } = ctx - const { feedItems, cursor, params } = state - const feed = feedService.views.formatFeed(feedItems, state, params.viewer) - return { feed, cursor } +const presentation = (inputs: { + ctx: Context + skeleton: Skeleton + hydration: HydrationState +}) => { + const { ctx, skeleton, hydration } = inputs + const feed = mapDefined(skeleton.uris, (uri) => + ctx.views.feedViewPost(uri, hydration), + ) + return { feed, cursor: skeleton.cursor } } type Context = { - db: Database - feedService: FeedService + hydrator: Hydrator + views: Views + dataplane: DataPlaneClient } type Params = QueryParams & { viewer: string } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + uris: string[] cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts index db143fc5b8c..18e12708e2a 100644 --- a/packages/bsky/src/api/app/bsky/feed/searchPosts.ts +++ b/packages/bsky/src/api/app/bsky/feed/searchPosts.ts @@ -1,17 +1,19 @@ import AppContext from '../../../../context' import { Server } from '../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' -import AtpAgent from '@atproto/api' import { mapDefined } from '@atproto/common' import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/searchPosts' -import { Database } from '../../../../db' import { - FeedHydrationState, - FeedRow, - FeedService, -} from '../../../../services/feed' -import { ActorService } from '../../../../services/actor' -import { createPipeline } from '../../../../pipeline' + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { DataPlaneClient } from '../../../../data-plane' +import { parseString } from '../../../../hydration/util' +import { creatorFromUri } from '../../../../views/util' export default function (server: Server, ctx: AppContext) { const searchPosts = createPipeline( @@ -24,19 +26,7 @@ export default function (server: Server, ctx: AppContext) { auth: ctx.authOptionalVerifier, handler: async ({ auth, params }) => { const viewer = auth.credentials.did - const db = ctx.db.getReplica('search') - const feedService = ctx.services.feed(db) - const actorService = ctx.services.actor(db) - const searchAgent = ctx.searchAgent - if (!searchAgent) { - throw new InvalidRequestError('Search not available') - } - - const results = await searchPosts( - { ...params, viewer }, - { db, feedService, actorService, searchAgent }, - ) - + const results = await searchPosts({ ...params, viewer }, ctx) return { encoding: 'application/json', body: results, @@ -45,86 +35,59 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const res = await ctx.searchAgent.api.app.bsky.unspecced.searchPostsSkeleton({ - q: params.q, - cursor: params.cursor, +const skeleton = async (inputs: SkeletonFnInput) => { + const { ctx, params } = inputs + const res = await ctx.dataplane.searchPosts({ + term: params.q, limit: params.limit, + cursor: params.cursor, }) - const postUris = res.data.posts.map((a) => a.uri) - const feedItems = await ctx.feedService.postUrisToFeedItems(postUris) return { - params, - feedItems, - cursor: res.data.cursor, - hitsTotal: res.data.hitsTotal, + posts: res.uris, + cursor: parseString(res.cursor), } } const hydration = async ( - state: SkeletonState, - ctx: Context, -): Promise => { - const { feedService } = ctx - const { params, feedItems } = state - const refs = feedService.feedItemRefs(feedItems) - const hydrated = await feedService.feedHydration({ - ...refs, - viewer: params.viewer, - }) - return { ...state, ...hydrated } + inputs: HydrationFnInput, +) => { + const { ctx, params, skeleton } = inputs + return ctx.hydrator.hydratePosts(skeleton.posts, params.viewer) } -const noBlocks = (state: HydrationState): HydrationState => { - const { viewer } = state.params - state.feedItems = state.feedItems.filter((item) => { - if (!viewer) return true - return !state.bam.block([viewer, item.postAuthorDid]) +const noBlocks = (inputs: RulesFnInput) => { + const { ctx, skeleton, hydration } = inputs + skeleton.posts = skeleton.posts.filter((uri) => { + const creator = creatorFromUri(uri) + return !ctx.views.viewerBlockExists(creator, hydration) }) - return state + return skeleton } -const presentation = (state: HydrationState, ctx: Context) => { - const { feedService, actorService } = ctx - const { feedItems, profiles, params } = state - const actors = actorService.views.profileBasicPresentation( - Object.keys(profiles), - state, - params.viewer, +const presentation = ( + inputs: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = inputs + const posts = mapDefined(skeleton.posts, (uri) => + ctx.views.post(uri, hydration), ) - - const postViews = mapDefined(feedItems, (item) => - feedService.views.formatPostView( - item.postUri, - actors, - state.posts, - state.threadgates, - state.embeds, - state.labels, - state.lists, - params.viewer, - ), - ) - return { posts: postViews, cursor: state.cursor, hitsTotal: state.hitsTotal } + return { + posts, + cursor: skeleton.cursor, + hitsTotal: skeleton.hitsTotal, + } } type Context = { - db: Database - feedService: FeedService - actorService: ActorService - searchAgent: AtpAgent + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views } type Params = QueryParams & { viewer: string | null } -type SkeletonState = { - params: Params - feedItems: FeedRow[] +type Skeleton = { + posts: string[] hitsTotal?: number cursor?: string } - -type HydrationState = SkeletonState & FeedHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts index 66b809d70ce..b42ca9eae36 100644 --- a/packages/bsky/src/api/app/bsky/graph/getBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getBlocks.ts @@ -1,47 +1,80 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getBlocks' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getBlocks = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getBlocks({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.did - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - let blocksReq = db.db - .selectFrom('actor_block') - .where('actor_block.creator', '=', requester) - .innerJoin('actor as subject', 'subject.did', 'actor_block.subjectDid') - .where(notSoftDeletedClause(ref('subject'))) - .selectAll('subject') - .select(['actor_block.cid as cid', 'actor_block.sortAt as sortAt']) - - const keyset = new TimeCidKeyset( - ref('actor_block.sortAt'), - ref('actor_block.cid'), - ) - blocksReq = paginate(blocksReq, { - limit, - cursor, - keyset, - }) - - const blocksRes = await blocksReq.execute() - - const actorService = ctx.services.actor(db) - const blocks = await actorService.views.profilesList(blocksRes, requester) - + const viewer = auth.credentials.did + const result = await getBlocks({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { - blocks, - cursor: keyset.packFromResult(blocksRes), - }, + body: result, } }, }) } + +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const { blockUris, cursor } = await ctx.hydrator.dataplane.getBlocks({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + const blocks = await ctx.hydrator.graph.getBlocks(blockUris) + const blockedDids = mapDefined( + blockUris, + (uri) => blocks.get(uri)?.record.subject, + ) + return { + blockedDids, + cursor: cursor || undefined, + } +} + +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { blockedDids } = skeleton + return ctx.hydrator.hydrateProfiles(blockedDids, viewer) +} + +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { blockedDids, cursor } = skeleton + const blocks = mapDefined(blockedDids, (did) => { + return ctx.views.profile(did, hydration) + }) + return { blocks, cursor } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + blockedDids: string[] + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts index 1382c1f87c7..dc89227f717 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollowers.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollowers.ts @@ -3,34 +3,34 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollowers' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { notSoftDeletedClause } from '../../../../db/util' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' -import { Actor } from '../../../../db/tables/actor' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' -import { createPipeline } from '../../../../pipeline' +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { didFromUri } from '../../../../hydration/util' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getFollowers = createPipeline( skeleton, hydration, - noBlocksInclInvalid, + noBlocks, presentation, ) server.app.bsky.graph.getFollowers({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = 'did' in auth.credentials ? auth.credentials.did : null const canViewTakendownProfile = auth.credentials.type === 'role' && auth.credentials.triage const result = await getFollowers( { ...params, viewer, canViewTakendownProfile }, - { db, actorService, graphService }, + ctx, ) return { @@ -41,91 +41,86 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params - const { ref } = db.db.dynamic - - const subject = await actorService.getActor(actor, canViewTakendownProfile) - if (!subject) { - throw new InvalidRequestError(`Actor not found: ${actor}`) +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]) + if (!subjectDid) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - - let followersReq = db.db - .selectFrom('follow') - .where('follow.subjectDid', '=', subject.did) - .innerJoin('actor as creator', 'creator.did', 'follow.creator') - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('creator'))), - ) - .selectAll('creator') - .select(['follow.cid as cid', 'follow.sortAt as sortAt']) - - const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) - followersReq = paginate(followersReq, { - limit, - cursor, - keyset, + const { followers, cursor } = await ctx.hydrator.graph.getActorFollowers({ + did: subjectDid, + cursor: params.cursor, + limit: params.limit, }) - - const followers = await followersReq.execute() return { - params, - followers, - subject, - cursor: keyset.packFromResult(followers), + subjectDid, + followUris: followers.map((f) => f.uri), + cursor: cursor || undefined, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, followers, subject } = state +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input const { viewer } = params - const [actors, bam] = await Promise.all([ - actorService.views.profiles([subject, ...followers], viewer), - graphService.getBlockAndMuteState( - followers.flatMap((item) => { - if (viewer) { - return [ - [viewer, item.did], - [subject.did, item.did], - ] - } - return [[subject.did, item.did]] - }), - ), - ]) - return { ...state, bam, actors } + const { followUris, subjectDid } = skeleton + const followState = await ctx.hydrator.hydrateFollows(followUris) + const dids = [subjectDid] + if (followState.follows) { + for (const [uri, follow] of followState.follows) { + if (follow) { + dids.push(didFromUri(uri)) + } + } + } + const profileState = await ctx.hydrator.hydrateProfiles(dids, viewer) + return mergeStates(followState, profileState) } -const noBlocksInclInvalid = (state: HydrationState) => { - const { subject } = state - const { viewer } = state.params - state.followers = state.followers.filter( - (item) => - !state.bam.block([subject.did, item.did]) && - (!viewer || !state.bam.block([viewer, item.did])), - ) - return state +const noBlocks = (input: RulesFnInput) => { + const { skeleton, params, hydration, ctx } = input + const { viewer } = params + skeleton.followUris = skeleton.followUris.filter((followUri) => { + const followerDid = didFromUri(followUri) + return ( + !hydration.followBlocks?.get(followUri) && + (!viewer || !ctx.views.viewerBlockExists(followerDid, hydration)) + ) + }) + return skeleton } -const presentation = (state: HydrationState) => { - const { params, followers, subject, actors, cursor } = state - const subjectView = actors[subject.did] - const followersView = mapDefined(followers, (item) => actors[item.did]) - if (!subjectView) { +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton, params } = input + const { subjectDid, followUris, cursor } = skeleton + const isTakendown = (did: string) => + ctx.views.actorIsTakendown(did, hydration) + + const subject = ctx.views.profile(subjectDid, hydration) + if ( + !subject || + (!params.canViewTakendownProfile && isTakendown(subjectDid)) + ) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - return { followers: followersView, subject: subjectView, cursor } + + const followers = mapDefined(followUris, (followUri) => { + const followerDid = didFromUri(followUri) + if (!params.canViewTakendownProfile && isTakendown(followerDid)) { + return + } + return ctx.views.profile(didFromUri(followUri), hydration) + }) + + return { followers, subject, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -134,13 +129,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - followers: Actor[] - subject: Actor + subjectDid: string + followUris: string[] cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap -} diff --git a/packages/bsky/src/api/app/bsky/graph/getFollows.ts b/packages/bsky/src/api/app/bsky/graph/getFollows.ts index 34b5d72a605..542380b0d16 100644 --- a/packages/bsky/src/api/app/bsky/graph/getFollows.ts +++ b/packages/bsky/src/api/app/bsky/graph/getFollows.ts @@ -1,36 +1,31 @@ import { mapDefined } from '@atproto/common' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollows' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getFollowers' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { notSoftDeletedClause } from '../../../../db/util' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' -import { Actor } from '../../../../db/tables/actor' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' -import { createPipeline } from '../../../../pipeline' +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { - const getFollows = createPipeline( - skeleton, - hydration, - noBlocksInclInvalid, - presentation, - ) + const getFollows = createPipeline(skeleton, hydration, noBlocks, presentation) server.app.bsky.graph.getFollows({ auth: ctx.authOptionalAccessOrRoleVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) const viewer = 'did' in auth.credentials ? auth.credentials.did : null const canViewTakendownProfile = auth.credentials.type === 'role' && auth.credentials.triage + // @TODO ensure canViewTakendownProfile gets threaded through and applied properly const result = await getFollows( { ...params, viewer, canViewTakendownProfile }, - { db, actorService, graphService }, + ctx, ) return { @@ -41,92 +36,89 @@ export default function (server: Server, ctx: AppContext) { }) } -const skeleton = async ( - params: Params, - ctx: Context, -): Promise => { - const { db, actorService } = ctx - const { limit, cursor, actor, canViewTakendownProfile } = params - const { ref } = db.db.dynamic - - const creator = await actorService.getActor(actor, canViewTakendownProfile) - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${actor}`) +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [subjectDid] = await ctx.hydrator.actor.getDidsDefined([params.actor]) + if (!subjectDid) { + throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - - let followsReq = db.db - .selectFrom('follow') - .where('follow.creator', '=', creator.did) - .innerJoin('actor as subject', 'subject.did', 'follow.subjectDid') - .if(!canViewTakendownProfile, (qb) => - qb.where(notSoftDeletedClause(ref('subject'))), - ) - .selectAll('subject') - .select(['follow.cid as cid', 'follow.sortAt as sortAt']) - - const keyset = new TimeCidKeyset(ref('follow.sortAt'), ref('follow.cid')) - followsReq = paginate(followsReq, { - limit, - cursor, - keyset, + const { follows, cursor } = await ctx.hydrator.graph.getActorFollows({ + did: subjectDid, + cursor: params.cursor, + limit: params.limit, }) - - const follows = await followsReq.execute() - return { - params, - follows, - creator, - cursor: keyset.packFromResult(follows), + subjectDid, + followUris: follows.map((f) => f.uri), + cursor: cursor || undefined, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService } = ctx - const { params, follows, creator } = state +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input const { viewer } = params - const [actors, bam] = await Promise.all([ - actorService.views.profiles([creator, ...follows], viewer), - graphService.getBlockAndMuteState( - follows.flatMap((item) => { - if (viewer) { - return [ - [viewer, item.did], - [creator.did, item.did], - ] - } - return [[creator.did, item.did]] - }), - ), - ]) - return { ...state, bam, actors } + const { followUris, subjectDid } = skeleton + const followState = await ctx.hydrator.hydrateFollows(followUris) + const dids = [subjectDid] + if (followState.follows) { + for (const follow of followState.follows.values()) { + if (follow) { + dids.push(follow.record.subject) + } + } + } + const profileState = await ctx.hydrator.hydrateProfiles(dids, viewer) + return mergeStates(followState, profileState) } -const noBlocksInclInvalid = (state: HydrationState) => { - const { creator } = state - const { viewer } = state.params - state.follows = state.follows.filter( - (item) => - !state.bam.block([creator.did, item.did]) && - (!viewer || !state.bam.block([viewer, item.did])), - ) - return state +const noBlocks = (input: RulesFnInput) => { + const { skeleton, params, hydration, ctx } = input + const { viewer } = params + skeleton.followUris = skeleton.followUris.filter((followUri) => { + const follow = hydration.follows?.get(followUri) + if (!follow) return false + return ( + !hydration.followBlocks?.get(followUri) && + (!viewer || + !ctx.views.viewerBlockExists(follow.record.subject, hydration)) + ) + }) + return skeleton } -const presentation = (state: HydrationState) => { - const { params, follows, creator, actors, cursor } = state - const creatorView = actors[creator.did] - const followsView = mapDefined(follows, (item) => actors[item.did]) - if (!creatorView) { +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton, params } = input + const { subjectDid, followUris, cursor } = skeleton + const isTakendown = (did: string) => + ctx.views.actorIsTakendown(did, hydration) + + const subject = ctx.views.profile(subjectDid, hydration) + if ( + !subject || + (!params.canViewTakendownProfile && isTakendown(subjectDid)) + ) { throw new InvalidRequestError(`Actor not found: ${params.actor}`) } - return { follows: followsView, subject: creatorView, cursor } + + const follows = mapDefined(followUris, (followUri) => { + const followDid = hydration.follows?.get(followUri)?.record.subject + if (!followDid) return + if (!params.canViewTakendownProfile && isTakendown(followDid)) { + return + } + return ctx.views.profile(followDid, hydration) + }) + + return { follows, subject, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -135,13 +127,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - follows: Actor[] - creator: Actor + subjectDid: string + followUris: string[] cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap -} diff --git a/packages/bsky/src/api/app/bsky/graph/getList.ts b/packages/bsky/src/api/app/bsky/graph/getList.ts index 82a70848cd9..d9c10926864 100644 --- a/packages/bsky/src/api/app/bsky/graph/getList.ts +++ b/packages/bsky/src/api/app/bsky/graph/getList.ts @@ -3,28 +3,24 @@ import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getList' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' -import { Actor } from '../../../../db/tables/actor' -import { GraphService, ListInfo } from '../../../../services/graph' -import { ActorService, ProfileHydrationState } from '../../../../services/actor' -import { createPipeline, noRules } from '../../../../pipeline' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator, mergeStates } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { ListItemInfo } from '../../../../data-plane/gen/bsky_pb' export default function (server: Server, ctx: AppContext) { const getList = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getList({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const graphService = ctx.services.graph(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.did - - const result = await getList( - { ...params, viewer }, - { db, graphService, actorService }, - ) - + const result = await getList({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -34,85 +30,57 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db, graphService } = ctx - const { list, limit, cursor, viewer } = params - const { ref } = db.db.dynamic - - const listRes = await graphService - .getListsQb(viewer) - .where('list.uri', '=', list) - .executeTakeFirst() - if (!listRes) { - throw new InvalidRequestError(`List not found: ${list}`) - } - - let itemsReq = graphService - .getListItemsQb() - .where('list_item.listUri', '=', list) - .where('list_item.creator', '=', listRes.creator) - - const keyset = new TimeCidKeyset( - ref('list_item.sortAt'), - ref('list_item.cid'), - ) - - itemsReq = paginate(itemsReq, { - limit, - cursor, - keyset, + const { ctx, params } = input + const { listitems, cursor } = await ctx.hydrator.dataplane.getListMembers({ + listUri: params.list, + limit: params.limit, + cursor: params.cursor, }) - - const listItems = await itemsReq.execute() - return { - params, - list: listRes, - listItems, - cursor: keyset.packFromResult(listItems), + listUri: params.list, + listitems, + cursor: cursor || undefined, } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const { params, list, listItems } = state - const profileState = await actorService.views.profileHydration( - [list, ...listItems].map((x) => x.did), - { viewer: params.viewer }, - ) - return { ...state, ...profileState } +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { listUri, listitems } = skeleton + const [listState, profileState] = await Promise.all([ + ctx.hydrator.hydrateLists([listUri], viewer), + ctx.hydrator.hydrateProfiles( + listitems.map(({ did }) => did), + viewer, + ), + ]) + return mergeStates(listState, profileState) } -const presentation = (state: HydrationState, ctx: Context) => { - const { actorService, graphService } = ctx - const { params, list, listItems, cursor, ...profileState } = state - const actors = actorService.views.profilePresentation( - Object.keys(profileState.profiles), - profileState, - params.viewer, - ) - const creator = actors[list.creator] - if (!creator) { - throw new InvalidRequestError(`Actor not found: ${list.handle}`) - } - const listView = graphService.formatListView(list, actors) - if (!listView) { - throw new InvalidRequestError('List not found') - } - const items = mapDefined(listItems, (item) => { - const subject = actors[item.did] +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUri, listitems, cursor } = skeleton + const list = ctx.views.list(listUri, hydration) + const items = mapDefined(listitems, ({ uri, did }) => { + const subject = ctx.views.profile(did, hydration) if (!subject) return - return { uri: item.uri, subject } + return { uri, subject } }) - return { list: listView, items, cursor } + if (!list) { + throw new InvalidRequestError('List not found') + } + return { list, items, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -120,10 +88,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - list: Actor & ListInfo - listItems: (Actor & { uri: string; cid: string; sortAt: string })[] + listUri: string + listitems: ListItemInfo[] cursor?: string } - -type HydrationState = SkeletonState & ProfileHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts index 03fd3496f97..885e8c7b875 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListBlocks.ts @@ -1,13 +1,16 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { Actor } from '../../../../db/tables/actor' -import { GraphService, ListInfo } from '../../../../services/graph' -import { ActorService, ProfileHydrationState } from '../../../../services/actor' -import { createPipeline, noRules } from '../../../../pipeline' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { const getListBlocks = createPipeline( @@ -19,16 +22,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.graph.getListBlocks({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const graphService = ctx.services.graph(db) - const actorService = ctx.services.actor(db) const viewer = auth.credentials.did - - const result = await getListBlocks( - { ...params, viewer }, - { db, actorService, graphService }, - ) - + const result = await getListBlocks({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -38,68 +33,37 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db, graphService } = ctx - const { limit, cursor, viewer } = params - const { ref } = db.db.dynamic - - let listsReq = graphService - .getListsQb(viewer) - .whereExists( - db.db - .selectFrom('list_block') - .where('list_block.creator', '=', viewer) - .whereRef('list_block.subjectUri', '=', ref('list.uri')) - .selectAll(), - ) - - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - - const listInfos = await listsReq.execute() - - return { - params, - listInfos, - cursor: keyset.packFromResult(listInfos), - } + const { ctx, params } = input + const { listUris, cursor } = + await ctx.hydrator.dataplane.getBlocklistSubscriptions({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { actorService } = ctx - const { params, listInfos } = state - const profileState = await actorService.views.profileHydration( - listInfos.map((list) => list.creator), - { viewer: params.viewer }, - ) - return { ...state, ...profileState } +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + return await ctx.hydrator.hydrateLists(skeleton.listUris, params.viewer) } -const presentation = (state: HydrationState, ctx: Context) => { - const { actorService, graphService } = ctx - const { params, listInfos, cursor, ...profileState } = state - const actors = actorService.views.profilePresentation( - Object.keys(profileState.profiles), - profileState, - params.viewer, - ) - const lists = mapDefined(listInfos, (list) => - graphService.formatListView(list, actors), - ) +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration)) return { lists, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -107,9 +71,6 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - listInfos: (Actor & ListInfo)[] + listUris: string[] cursor?: string } - -type HydrationState = SkeletonState & ProfileHydrationState diff --git a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts index ab0ac77f47c..1911204cbb1 100644 --- a/packages/bsky/src/api/app/bsky/graph/getListMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getListMutes.ts @@ -1,51 +1,76 @@ import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getListBlocks' import AppContext from '../../../../context' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getListMutes = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.graph.getListMutes({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.did - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic + const viewer = auth.credentials.did + const result = await getListMutes({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, + } + }, + }) +} - const graphService = ctx.services.graph(db) +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { ctx, params } = input + const { listUris, cursor } = + await ctx.hydrator.dataplane.getMutelistSubscriptions({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } +} - let listsReq = graphService - .getListsQb(requester) - .whereExists( - db.db - .selectFrom('list_mute') - .where('list_mute.mutedByDid', '=', requester) - .whereRef('list_mute.listUri', '=', ref('list.uri')) - .selectAll(), - ) +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + return await ctx.hydrator.hydrateLists(skeleton.listUris, params.viewer) +} - const keyset = new TimeCidKeyset(ref('list.createdAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) - const listsRes = await listsReq.execute() +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => ctx.views.list(uri, hydration)) + return { lists, cursor } +} - const actorService = ctx.services.actor(db) - const profiles = await actorService.views.profiles(listsRes, requester) +type Context = { + hydrator: Hydrator + views: Views +} - const lists = mapDefined(listsRes, (row) => - graphService.formatListView(row, profiles), - ) +type Params = QueryParams & { + viewer: string +} - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) +type SkeletonState = { + listUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getLists.ts b/packages/bsky/src/api/app/bsky/graph/getLists.ts index 73deb51900b..247ed50437f 100644 --- a/packages/bsky/src/api/app/bsky/graph/getLists.ts +++ b/packages/bsky/src/api/app/bsky/graph/getLists.ts @@ -1,56 +1,74 @@ import { mapDefined } from '@atproto/common' -import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getLists' import AppContext from '../../../../context' +import { + createPipeline, + HydrationFnInput, + noRules, + PresentationFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getLists = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getLists({ auth: ctx.authOptionalVerifier, handler: async ({ params, auth }) => { - const { actor, limit, cursor } = params - const requester = auth.credentials.did - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) - - const creatorRes = await actorService.getActor(actor) - if (!creatorRes) { - throw new InvalidRequestError(`Actor not found: ${actor}`) + const viewer = auth.credentials.did + const result = await getLists({ ...params, viewer }, ctx) + return { + encoding: 'application/json', + body: result, } + }, + }) +} - let listsReq = graphService - .getListsQb(requester) - .where('list.creator', '=', creatorRes.did) +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { ctx, params } = input + const { listUris, cursor } = await ctx.hydrator.dataplane.getActorLists({ + actorDid: params.actor, + cursor: params.cursor, + limit: params.limit, + }) + return { listUris, cursor: cursor || undefined } +} - const keyset = new TimeCidKeyset(ref('list.sortAt'), ref('list.cid')) - listsReq = paginate(listsReq, { - limit, - cursor, - keyset, - }) +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { listUris } = skeleton + return ctx.hydrator.hydrateLists(listUris, viewer) +} - const [listsRes, profiles] = await Promise.all([ - listsReq.execute(), - actorService.views.profiles([creatorRes], requester), - ]) - if (!profiles[creatorRes.did]) { - throw new InvalidRequestError(`Actor not found: ${actor}`) - } +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, skeleton, hydration } = input + const { listUris, cursor } = skeleton + const lists = mapDefined(listUris, (uri) => { + return ctx.views.list(uri, hydration) + }) + return { lists, cursor } +} - const lists = mapDefined(listsRes, (row) => - graphService.formatListView(row, profiles), - ) +type Context = { + hydrator: Hydrator + views: Views +} - return { - encoding: 'application/json', - body: { - lists, - cursor: keyset.packFromResult(listsRes), - }, - } - }, - }) +type Params = QueryParams & { + viewer: string | null +} + +type SkeletonState = { + listUris: string[] + cursor?: string } diff --git a/packages/bsky/src/api/app/bsky/graph/getMutes.ts b/packages/bsky/src/api/app/bsky/graph/getMutes.ts index 9b334cf4829..4559029d43d 100644 --- a/packages/bsky/src/api/app/bsky/graph/getMutes.ts +++ b/packages/bsky/src/api/app/bsky/graph/getMutes.ts @@ -1,46 +1,75 @@ +import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' -import { CreatedAtDidKeyset, paginate } from '../../../../db/pagination' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getMutes' import AppContext from '../../../../context' -import { notSoftDeletedClause } from '../../../../db/util' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' export default function (server: Server, ctx: AppContext) { + const getMutes = createPipeline(skeleton, hydration, noRules, presentation) server.app.bsky.graph.getMutes({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const { limit, cursor } = params - const requester = auth.credentials.did - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - - let mutesReq = db.db - .selectFrom('mute') - .innerJoin('actor', 'actor.did', 'mute.subjectDid') - .where(notSoftDeletedClause(ref('actor'))) - .where('mute.mutedByDid', '=', requester) - .selectAll('actor') - .select('mute.createdAt as createdAt') - - const keyset = new CreatedAtDidKeyset( - ref('mute.createdAt'), - ref('mute.subjectDid'), - ) - mutesReq = paginate(mutesReq, { - limit, - cursor, - keyset, - }) - - const mutesRes = await mutesReq.execute() - - const actorService = ctx.services.actor(db) - + const viewer = auth.credentials.did + const result = await getMutes({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { - cursor: keyset.packFromResult(mutesRes), - mutes: await actorService.views.profilesList(mutesRes, requester), - }, + body: result, } }, }) } + +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const { dids, cursor } = await ctx.hydrator.dataplane.getMutes({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }) + return { + mutedDids: dids, + cursor: cursor || undefined, + } +} + +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { mutedDids } = skeleton + return ctx.hydrator.hydrateProfiles(mutedDids, viewer) +} + +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { mutedDids, cursor } = skeleton + const mutes = mapDefined(mutedDids, (did) => { + return ctx.views.profile(did, hydration) + }) + return { mutes, cursor } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + mutedDids: string[] + cursor?: string +} diff --git a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts index eddf0cd5fd6..1840f84b3c6 100644 --- a/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +++ b/packages/bsky/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts @@ -1,139 +1,98 @@ -import { sql } from 'kysely' +import { mapDefined } from '@atproto/common' +import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' +import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/getSuggestedFollowsByActor' import AppContext from '../../../../context' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { Database } from '../../../../db' -import { ActorService } from '../../../../services/actor' - -const RESULT_LENGTH = 10 +import { + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, + createPipeline, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getSuggestedFollowsByActor = createPipeline( + skeleton, + hydration, + noBlocksOrMutes, + presentation, + ) server.app.bsky.graph.getSuggestedFollowsByActor({ auth: ctx.authVerifier, handler: async ({ auth, params }) => { - const { actor } = params const viewer = auth.credentials.did - - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const actorDid = await actorService.getActorDid(actor) - - if (!actorDid) { - throw new InvalidRequestError('Actor not found') - } - - const skeleton = await getSkeleton( - { - actor: actorDid, - viewer, - }, - { - db, - actorService, - }, - ) - const hydrationState = await actorService.views.profileDetailHydration( - skeleton.map((a) => a.did), - { viewer }, - ) - const presentationState = actorService.views.profileDetailPresentation( - skeleton.map((a) => a.did), - hydrationState, - { viewer }, + const result = await getSuggestedFollowsByActor( + { ...params, viewer }, + ctx, ) - const suggestions = Object.values(presentationState).filter((profile) => { - return ( - !profile.viewer?.muted && - !profile.viewer?.mutedByList && - !profile.viewer?.blocking && - !profile.viewer?.blockedBy - ) - }) - return { encoding: 'application/json', - body: { suggestions }, + body: result, } }, }) } -async function getSkeleton( - params: { - actor: string - viewer: string - }, - ctx: { - db: Database - actorService: ActorService - }, -): Promise<{ did: string }[]> { - const actorsViewerFollows = ctx.db.db - .selectFrom('follow') - .where('creator', '=', params.viewer) - .select('subjectDid') - const mostLikedAccounts = await ctx.db.db - .selectFrom( - ctx.db.db - .selectFrom('like') - .where('creator', '=', params.actor) - .select(sql`split_part(subject, '/', 3)`.as('subjectDid')) - .orderBy('sortAt', 'desc') - .limit(1000) // limit to 1000 - .as('likes'), - ) - .select('likes.subjectDid as did') - .select((qb) => qb.fn.count('likes.subjectDid').as('count')) - .where('likes.subjectDid', 'not in', actorsViewerFollows) - .where('likes.subjectDid', 'not in', [params.actor, params.viewer]) - .groupBy('likes.subjectDid') - .orderBy('count', 'desc') - .limit(RESULT_LENGTH) - .execute() - const resultDids = mostLikedAccounts.map((a) => ({ did: a.did })) as { - did: string - }[] +const skeleton = async (input: SkeletonFnInput) => { + const { params, ctx } = input + const [relativeToDid] = await ctx.hydrator.actor.getDids([params.actor]) + if (!relativeToDid) { + throw new InvalidRequestError('Actor not found') + } + const { dids, cursor } = await ctx.hydrator.dataplane.getFollowSuggestions({ + actorDid: params.viewer, + relativeToDid, + }) + return { + suggestedDids: dids, + cursor: cursor || undefined, + } +} + +const hydration = async ( + input: HydrationFnInput, +) => { + const { ctx, params, skeleton } = input + const { viewer } = params + const { suggestedDids } = skeleton + return ctx.hydrator.hydrateProfilesDetailed(suggestedDids, viewer) +} - if (resultDids.length < RESULT_LENGTH) { - // backfill with popular accounts followed by actor - const mostPopularAccountsActorFollows = await ctx.db.db - .selectFrom('follow') - .innerJoin('profile_agg', 'follow.subjectDid', 'profile_agg.did') - .select('follow.subjectDid as did') - .where('follow.creator', '=', params.actor) - .where('follow.subjectDid', '!=', params.viewer) - .where('follow.subjectDid', 'not in', actorsViewerFollows) - .if(resultDids.length > 0, (qb) => - qb.where( - 'subjectDid', - 'not in', - resultDids.map((a) => a.did), - ), - ) - .orderBy('profile_agg.followersCount', 'desc') - .limit(RESULT_LENGTH) - .execute() +const noBlocksOrMutes = ( + input: RulesFnInput, +) => { + const { ctx, skeleton, hydration } = input + skeleton.suggestedDids = skeleton.suggestedDids.filter( + (did) => + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration), + ) + return skeleton +} - resultDids.push(...mostPopularAccountsActorFollows) - } +const presentation = ( + input: PresentationFnInput, +) => { + const { ctx, hydration, skeleton } = input + const { suggestedDids } = skeleton + const suggestions = mapDefined(suggestedDids, (did) => + ctx.views.profileDetailed(did, hydration), + ) + return { suggestions } +} - if (resultDids.length < RESULT_LENGTH) { - // backfill with suggested_follow table - const additional = await ctx.db.db - .selectFrom('suggested_follow') - .where( - 'did', - 'not in', - // exclude any we already have - resultDids.map((a) => a.did).concat([params.actor, params.viewer]), - ) - // and aren't already followed by viewer - .where('did', 'not in', actorsViewerFollows) - .selectAll() - .execute() +type Context = { + hydrator: Hydrator + views: Views +} - resultDids.push(...additional) - } +type Params = QueryParams & { + viewer: string +} - return resultDids +type SkeletonState = { + suggestedDids: string[] } diff --git a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts index c23d7683abe..f38ec193aa9 100644 --- a/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts +++ b/packages/bsky/src/api/app/bsky/notification/getUnreadCount.ts @@ -1,43 +1,74 @@ -import { sql } from 'kysely' import { InvalidRequestError } from '@atproto/xrpc-server' import { Server } from '../../../../lexicon' -import { countAll, notSoftDeletedClause } from '../../../../db/util' +import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/getUnreadCount' import AppContext from '../../../../context' +import { + HydrationFnInput, + PresentationFnInput, + SkeletonFnInput, + createPipeline, + noRules, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' export default function (server: Server, ctx: AppContext) { + const getUnreadCount = createPipeline( + skeleton, + hydration, + noRules, + presentation, + ) server.app.bsky.notification.getUnreadCount({ auth: ctx.authVerifier, handler: async ({ auth, params }) => { - const requester = auth.credentials.did - if (params.seenAt) { - throw new InvalidRequestError('The seenAt parameter is unsupported') - } - - const db = ctx.db.getReplica() - const { ref } = db.db.dynamic - const result = await db.db - .selectFrom('notification') - .select(countAll.as('count')) - .innerJoin('actor', 'actor.did', 'notification.did') - .leftJoin('actor_state', 'actor_state.did', 'actor.did') - .innerJoin('record', 'record.uri', 'notification.recordUri') - .where(notSoftDeletedClause(ref('actor'))) - .where(notSoftDeletedClause(ref('record'))) - // Ensure to hit notification_did_sortat_idx, handling case where lastSeenNotifs is null. - .where('notification.did', '=', requester) - .where( - 'notification.sortAt', - '>', - sql`coalesce(${ref('actor_state.lastSeenNotifs')}, ${''})`, - ) - .executeTakeFirst() - - const count = result?.count ?? 0 - + const viewer = auth.credentials.did + const result = await getUnreadCount({ ...params, viewer }, ctx) return { encoding: 'application/json', - body: { count }, + body: result, } }, }) } + +const skeleton = async ( + input: SkeletonFnInput, +): Promise => { + const { params, ctx } = input + if (params.seenAt) { + throw new InvalidRequestError('The seenAt parameter is unsupported') + } + const res = await ctx.hydrator.dataplane.getUnreadNotificationCount({ + actorDid: params.viewer, + }) + return { + count: res.count, + } +} + +const hydration = async ( + _input: HydrationFnInput, +) => { + return {} +} + +const presentation = ( + input: PresentationFnInput, +) => { + const { skeleton } = input + return { count: skeleton.count } +} + +type Context = { + hydrator: Hydrator + views: Views +} + +type Params = QueryParams & { + viewer: string +} + +type SkeletonState = { + count: number +} diff --git a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts index 0013d13a7b0..b977cbfe177 100644 --- a/packages/bsky/src/api/app/bsky/notification/listNotifications.ts +++ b/packages/bsky/src/api/app/bsky/notification/listNotifications.ts @@ -1,16 +1,19 @@ import { InvalidRequestError } from '@atproto/xrpc-server' -import { jsonStringToLex } from '@atproto/lexicon' import { mapDefined } from '@atproto/common' import { Server } from '../../../../lexicon' import { QueryParams } from '../../../../lexicon/types/app/bsky/notification/listNotifications' import AppContext from '../../../../context' -import { Database } from '../../../../db' -import { notSoftDeletedClause } from '../../../../db/util' -import { paginate, TimeCidKeyset } from '../../../../db/pagination' -import { BlockAndMuteState, GraphService } from '../../../../services/graph' -import { ActorInfoMap, ActorService } from '../../../../services/actor' -import { getSelfLabels, Labels, LabelService } from '../../../../services/label' -import { createPipeline } from '../../../../pipeline' +import { + createPipeline, + HydrationFnInput, + PresentationFnInput, + RulesFnInput, + SkeletonFnInput, +} from '../../../../pipeline' +import { Hydrator } from '../../../../hydration/hydrator' +import { Views } from '../../../../views' +import { Notification } from '../../../../data-plane/gen/bsky_pb' +import { didFromUri } from '../../../../hydration/util' export default function (server: Server, ctx: AppContext) { const listNotifications = createPipeline( @@ -22,17 +25,8 @@ export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.listNotifications({ auth: ctx.authVerifier, handler: async ({ params, auth }) => { - const db = ctx.db.getReplica() - const actorService = ctx.services.actor(db) - const graphService = ctx.services.graph(db) - const labelService = ctx.services.label(db) const viewer = auth.credentials.did - - const result = await listNotifications( - { ...params, viewer }, - { db, actorService, graphService, labelService }, - ) - + const result = await listNotifications({ ...params, viewer }, ctx) return { encoding: 'application/json', body: result, @@ -42,138 +36,64 @@ export default function (server: Server, ctx: AppContext) { } const skeleton = async ( - params: Params, - ctx: Context, + input: SkeletonFnInput, ): Promise => { - const { db } = ctx - const { limit, cursor, viewer } = params - const { ref } = db.db.dynamic + const { params, ctx } = input if (params.seenAt) { throw new InvalidRequestError('The seenAt parameter is unsupported') } - let notifBuilder = db.db - .selectFrom('notification as notif') - .where('notif.did', '=', viewer) - .where((clause) => - clause - .where('reasonSubject', 'is', null) - .orWhereExists( - db.db - .selectFrom('record as subject') - .selectAll() - .whereRef('subject.uri', '=', ref('notif.reasonSubject')), - ), - ) - .select([ - 'notif.author as authorDid', - 'notif.recordUri as uri', - 'notif.recordCid as cid', - 'notif.reason as reason', - 'notif.reasonSubject as reasonSubject', - 'notif.sortAt as indexedAt', - ]) - - const keyset = new NotifsKeyset(ref('notif.sortAt'), ref('notif.recordCid')) - notifBuilder = paginate(notifBuilder, { - cursor, - limit, - keyset, - tryIndex: true, - }) - - const actorStateQuery = db.db - .selectFrom('actor_state') - .selectAll() - .where('did', '=', viewer) - - const [notifs, actorState] = await Promise.all([ - notifBuilder.execute(), - actorStateQuery.executeTakeFirst(), + const [res, lastSeenRes] = await Promise.all([ + ctx.hydrator.dataplane.getNotifications({ + actorDid: params.viewer, + cursor: params.cursor, + limit: params.limit, + }), + ctx.hydrator.dataplane.getNotificationSeen({ + actorDid: params.viewer, + }), ]) - return { - params, - notifs, - cursor: keyset.packFromResult(notifs), - lastSeenNotifs: actorState?.lastSeenNotifs, + notifs: res.notifications, + cursor: res.cursor, + lastSeenNotifs: lastSeenRes.timestamp?.toDate().toISOString(), } } -const hydration = async (state: SkeletonState, ctx: Context) => { - const { graphService, actorService, labelService, db } = ctx - const { params, notifs } = state - const { viewer } = params - const dids = notifs.map((notif) => notif.authorDid) - const uris = notifs.map((notif) => notif.uri) - const [actors, records, labels, bam] = await Promise.all([ - actorService.views.profiles(dids, viewer), - getRecordMap(db, uris), - labelService.getLabelsForUris(uris), - graphService.getBlockAndMuteState(dids.map((did) => [viewer, did])), - ]) - return { ...state, actors, records, labels, bam } -} - -const noBlockOrMutes = (state: HydrationState) => { - const { viewer } = state.params - state.notifs = state.notifs.filter( - (item) => - !state.bam.block([viewer, item.authorDid]) && - !state.bam.mute([viewer, item.authorDid]), - ) - return state +const hydration = async ( + input: HydrationFnInput, +) => { + const { skeleton, params, ctx } = input + return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.viewer) } -const presentation = (state: HydrationState) => { - const { notifs, cursor, actors, records, labels, lastSeenNotifs } = state - const notifications = mapDefined(notifs, (notif) => { - const author = actors[notif.authorDid] - const record = records[notif.uri] - if (!author || !record) return undefined - const recordLabels = labels[notif.uri] ?? [] - const recordSelfLabels = getSelfLabels({ - uri: notif.uri, - cid: notif.cid, - record, - }) - return { - uri: notif.uri, - cid: notif.cid, - author, - reason: notif.reason, - reasonSubject: notif.reasonSubject || undefined, - record, - isRead: lastSeenNotifs ? notif.indexedAt <= lastSeenNotifs : false, - indexedAt: notif.indexedAt, - labels: [...recordLabels, ...recordSelfLabels], - } +const noBlockOrMutes = ( + input: RulesFnInput, +) => { + const { skeleton, hydration, ctx } = input + skeleton.notifs = skeleton.notifs.filter((item) => { + const did = didFromUri(item.uri) + return ( + !ctx.views.viewerBlockExists(did, hydration) && + !ctx.views.viewerMuteExists(did, hydration) + ) }) - return { notifications, cursor } + return skeleton } -const getRecordMap = async ( - db: Database, - uris: string[], -): Promise => { - if (!uris.length) return {} - const { ref } = db.db.dynamic - const recordRows = await db.db - .selectFrom('record') - .select(['uri', 'json']) - .where('uri', 'in', uris) - .where(notSoftDeletedClause(ref('record'))) - .execute() - return recordRows.reduce((acc, { uri, json }) => { - acc[uri] = jsonStringToLex(json) as Record - return acc - }, {} as RecordMap) +const presentation = ( + input: PresentationFnInput, +) => { + const { skeleton, hydration, ctx } = input + const { notifs, lastSeenNotifs, cursor } = skeleton + const notifications = mapDefined(notifs, (notif) => + ctx.views.notification(notif, lastSeenNotifs, hydration), + ) + return { notifications, cursor } } type Context = { - db: Database - actorService: ActorService - graphService: GraphService - labelService: LabelService + hydrator: Hydrator + views: Views } type Params = QueryParams & { @@ -181,32 +101,7 @@ type Params = QueryParams & { } type SkeletonState = { - params: Params - notifs: NotifRow[] + notifs: Notification[] lastSeenNotifs?: string cursor?: string } - -type HydrationState = SkeletonState & { - bam: BlockAndMuteState - actors: ActorInfoMap - records: RecordMap - labels: Labels -} - -type RecordMap = { [uri: string]: Record } - -type NotifRow = { - authorDid: string - uri: string - cid: string - reason: string - reasonSubject: string | null - indexedAt: string -} - -class NotifsKeyset extends TimeCidKeyset { - labelResult(result: NotifRow) { - return { primary: result.indexedAt, secondary: result.cid } - } -} diff --git a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts index b7c705c0889..09938d316d1 100644 --- a/packages/bsky/src/api/app/bsky/notification/updateSeen.ts +++ b/packages/bsky/src/api/app/bsky/notification/updateSeen.ts @@ -1,7 +1,6 @@ +import { Timestamp } from '@bufbuild/protobuf' import { Server } from '../../../../lexicon' -import { InvalidRequestError } from '@atproto/xrpc-server' import AppContext from '../../../../context' -import { excluded } from '../../../../db/util' export default function (server: Server, ctx: AppContext) { server.app.bsky.notification.updateSeen({ @@ -9,25 +8,10 @@ export default function (server: Server, ctx: AppContext) { handler: async ({ input, auth }) => { const { seenAt } = input.body const viewer = auth.credentials.did - - let parsed: string - try { - parsed = new Date(seenAt).toISOString() - } catch (_err) { - throw new InvalidRequestError('Invalid date') - } - - const db = ctx.db.getPrimary() - - await db.db - .insertInto('actor_state') - .values({ did: viewer, lastSeenNotifs: parsed }) - .onConflict((oc) => - oc.column('did').doUpdateSet({ - lastSeenNotifs: excluded(db.db, 'lastSeenNotifs'), - }), - ) - .executeTakeFirst() + await ctx.dataplane.updateNotificationSeen({ + actorDid: viewer, + timestamp: Timestamp.fromDate(new Date(seenAt)), + }) }, }) } diff --git a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts index 821eeda655f..c382fd4cb4f 100644 --- a/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts +++ b/packages/bsky/src/api/app/bsky/unspecced/getTimelineSkeleton.ts @@ -1,23 +1,32 @@ import { Server } from '../../../../lexicon' +import { ids } from '../../../../lexicon/lexicons' import AppContext from '../../../../context' import { skeleton } from '../feed/getTimeline' import { toSkeletonItem } from '../../../../feed-gen/types' +import { urisByCollection } from '../../../../hydration/util' // THIS IS A TEMPORARY UNSPECCED ROUTE export default function (server: Server, ctx: AppContext) { server.app.bsky.unspecced.getTimelineSkeleton({ auth: ctx.authVerifier, handler: async ({ auth, params }) => { - const db = ctx.db.getReplica('timeline') - const feedService = ctx.services.feed(db) const viewer = auth.credentials.did - - const result = await skeleton({ ...params, viewer }, { db, feedService }) - + const result = await skeleton({ ctx, params: { ...params, viewer } }) + const collections = urisByCollection(result.uris) + const reposts = await ctx.hydrator.feed.getReposts( + collections.get(ids.AppBskyFeedRepost) ?? [], + ) + const feed = result.uris.map((uri) => { + const repost = reposts.get(uri) + return toSkeletonItem({ + itemUri: uri, + postUri: repost ? repost.record.subject.uri : uri, + }) + }) return { encoding: 'application/json', body: { - feed: result.feedItems.map(toSkeletonItem), + feed, cursor: result.cursor, }, } diff --git a/packages/bsky/src/config.ts b/packages/bsky/src/config.ts index 679f99c59a4..b2808326fa6 100644 --- a/packages/bsky/src/config.ts +++ b/packages/bsky/src/config.ts @@ -12,6 +12,7 @@ export interface ServerConfigValues { dbReplicaPostgresUrls?: string[] dbReplicaTags?: Record // E.g. { timeline: [0], thread: [1] } dbPostgresSchema?: string + dataplaneUrl: string didPlcUrl: string didCacheStaleTTL: number didCacheMaxTTL: number @@ -74,6 +75,8 @@ export class ServerConfig { ) const dbPostgresSchema = process.env.DB_POSTGRES_SCHEMA assert(dbPrimaryPostgresUrl) + const dataplaneUrl = process.env.DATAPLANE_URL + assert(dataplaneUrl) const adminPassword = process.env.ADMIN_PASSWORD || 'admin' const moderatorPassword = process.env.MODERATOR_PASSWORD || undefined const triagePassword = process.env.TRIAGE_PASSWORD || undefined @@ -93,6 +96,7 @@ export class ServerConfig { dbReplicaPostgresUrls, dbReplicaTags, dbPostgresSchema, + dataplaneUrl, didPlcUrl, didCacheStaleTTL, didCacheMaxTTL, @@ -162,6 +166,10 @@ export class ServerConfig { return this.cfg.dbPostgresSchema } + get dataplaneUrl() { + return this.cfg.dataplaneUrl + } + get didCacheStaleTTL() { return this.cfg.didCacheStaleTTL } diff --git a/packages/bsky/src/context.ts b/packages/bsky/src/context.ts index 3488c6a5c02..9827badf644 100644 --- a/packages/bsky/src/context.ts +++ b/packages/bsky/src/context.ts @@ -13,6 +13,9 @@ import { BackgroundQueue } from './background' import { MountedAlgos } from './feed-gen/types' import { LabelCache } from './label-cache' import { NotificationServer } from './notifications' +import { DataPlaneClient } from './data-plane/client' +import { Hydrator } from './hydration/hydrator' +import { Views } from './views' export class AppContext { public moderationPushAgent: AtpAgent | undefined @@ -22,6 +25,9 @@ export class AppContext { imgUriBuilder: ImageUriBuilder cfg: ServerConfig services: Services + dataplane: DataPlaneClient + hydrator: Hydrator + views: Views signingKey: Keypair idResolver: IdResolver didCache: DidSqlCache @@ -58,6 +64,18 @@ export class AppContext { return this.opts.services } + get dataplane(): DataPlaneClient { + return this.opts.dataplane + } + + get hydrator(): Hydrator { + return this.opts.hydrator + } + + get views(): Views { + return this.opts.views + } + get signingKey(): Keypair { return this.opts.signingKey } diff --git a/packages/bsky/src/data-plane/gen/bsky_connect.ts b/packages/bsky/src/data-plane/gen/bsky_connect.ts index 482f0c061ac..c61bdd2ad2e 100644 --- a/packages/bsky/src/data-plane/gen/bsky_connect.ts +++ b/packages/bsky/src/data-plane/gen/bsky_connect.ts @@ -1,9 +1,9 @@ -// @generated by protoc-gen-connect-es v1.1.4 with parameter "target=ts" +// @generated by protoc-gen-connect-es v1.1.4 with parameter "target=ts,import_extension=.ts" // @generated from file bsky.proto (package bsky, syntax proto3) /* eslint-disable */ // @ts-nocheck -import { GetActorFeedsRequest, GetActorFeedsResponse, GetActorFollowsActorsRequest, GetActorFollowsActorsResponse, GetActorLikesRequest, GetActorLikesResponse, GetActorMutesActorRequest, GetActorMutesActorResponse, GetActorMutesActorViaListRequest, GetActorMutesActorViaListResponse, GetActorRepostsRequest, GetActorRepostsResponse, GetAuthorFeedRequest, GetAuthorFeedResponse, GetBidirectionalBlockRequest, GetBidirectionalBlockResponse, GetBidirectionalBlockViaListRequest, GetBidirectionalBlockViaListResponse, GetBlobTakedownRequest, GetBlobTakedownResponse, GetBlocklistSubscriptionRequest, GetBlocklistSubscriptionResponse, GetBlocklistSubscriptionsRequest, GetBlocklistSubscriptionsResponse, GetBlocksRequest, GetBlocksResponse, GetDidsByHandlesRequest, GetDidsByHandlesResponse, GetFeedGeneratorsRequest, GetFeedGeneratorsResponse, GetFeedGeneratorStatusRequest, GetFeedGeneratorStatusResponse, GetFollowersCountRequest, GetFollowersCountResponse, GetFollowersRequest, GetFollowersResponse, GetFollowsCountRequest, GetFollowsCountResponse, GetFollowsRequest, GetFollowsResponse, GetHandlesRequest, GetHandlesResponse, GetLabelsRequest, GetLabelsResponse, GetLatestRevRequest, GetLatestRevResponse, GetLikeByActorAndSubjectRequest, GetLikeByActorAndSubjectResponse, GetLikesBySubjectRequest, GetLikesBySubjectResponse, GetLikesCountRequest, GetLikesCountResponse, GetListCountRequest, GetListCountResponse, GetListFeedRequest, GetListFeedResponse, GetListMembershipRequest, GetListMembershipResponse, GetListMembersRequest, GetListMembersResponse, GetListRequest, GetListResponse, GetMutelistSubscriptionRequest, GetMutelistSubscriptionResponse, GetMutelistSubscriptionsRequest, GetMutelistSubscriptionsResponse, GetMutesRequest, GetMutesResponse, GetNotificationSeenRequest, GetNotificationSeenResponse, GetNotificationsRequest, GetNotificationsResponse, GetPostReplyCountRequest, GetPostReplyCountResponse, GetPostsRequest, GetPostsResponse, GetProfilesRequest, GetProfilesResponse, GetRepostByActorAndSubjectRequest, GetRepostByActorAndSubjectResponse, GetRepostsBySubjectRequest, GetRepostsBySubjectResponse, GetRepostsCountRequest, GetRepostsCountResponse, GetSuggestedFeedsRequest, GetSuggestedFeedsResponse, GetSuggestionsRequest, GetSuggestionsResponse, GetThreadgatesRequest, GetThreadgatesResponse, GetThreadRequest, GetThreadResponse, GetTimelineRequest, GetTimelineResponse, GetUnreadNotificationCountRequest, GetUnreadNotificationCountResponse, PingRequest, PingResponse, SearchActorsRequest, SearchActorsResponse, SearchPostsRequest, SearchPostsResponse, UpdateNotificationSeenRequest, UpdateNotificationSeenResponse, UpdateTakedownRequest, UpdateTakedownResponse } from "./bsky_pb"; +import { GetActorFeedsRequest, GetActorFeedsResponse, GetActorFollowsActorsRequest, GetActorFollowsActorsResponse, GetActorLikesRequest, GetActorLikesResponse, GetActorListsRequest, GetActorListsResponse, GetActorMutesActorRequest, GetActorMutesActorResponse, GetActorMutesActorViaListRequest, GetActorMutesActorViaListResponse, GetActorRepostsRequest, GetActorRepostsResponse, GetActorsRequest, GetActorsResponse, GetAuthorFeedRequest, GetAuthorFeedResponse, GetBidirectionalBlockRequest, GetBidirectionalBlockResponse, GetBidirectionalBlockViaListRequest, GetBidirectionalBlockViaListResponse, GetBlobTakedownRequest, GetBlobTakedownResponse, GetBlockExistenceRequest, GetBlockExistenceResponse, GetBlocklistSubscriptionRequest, GetBlocklistSubscriptionResponse, GetBlocklistSubscriptionsRequest, GetBlocklistSubscriptionsResponse, GetBlockRecordsRequest, GetBlockRecordsResponse, GetBlocksRequest, GetBlocksResponse, GetDidsByHandlesRequest, GetDidsByHandlesResponse, GetFeedGeneratorRecordsRequest, GetFeedGeneratorRecordsResponse, GetFeedGeneratorStatusRequest, GetFeedGeneratorStatusResponse, GetFollowCountsRequest, GetFollowCountsResponse, GetFollowerCountsRequest, GetFollowerCountsResponse, GetFollowersRequest, GetFollowersResponse, GetFollowRecordsRequest, GetFollowRecordsResponse, GetFollowsRequest, GetFollowsResponse, GetFollowSuggestionsRequest, GetFollowSuggestionsResponse, GetLabelsRequest, GetLabelsResponse, GetLatestRevRequest, GetLatestRevResponse, GetLikeCountsRequest, GetLikeCountsResponse, GetLikeRecordsRequest, GetLikeRecordsResponse, GetLikesByActorAndSubjectsRequest, GetLikesByActorAndSubjectsResponse, GetLikesBySubjectRequest, GetLikesBySubjectResponse, GetListBlockRecordsRequest, GetListBlockRecordsResponse, GetListCountRequest, GetListCountResponse, GetListFeedRequest, GetListFeedResponse, GetListItemRecordsRequest, GetListItemRecordsResponse, GetListMembershipRequest, GetListMembershipResponse, GetListMembersRequest, GetListMembersResponse, GetListRecordsRequest, GetListRecordsResponse, GetMutelistSubscriptionRequest, GetMutelistSubscriptionResponse, GetMutelistSubscriptionsRequest, GetMutelistSubscriptionsResponse, GetMutesRequest, GetMutesResponse, GetNotificationSeenRequest, GetNotificationSeenResponse, GetNotificationsRequest, GetNotificationsResponse, GetPostCountsRequest, GetPostCountsResponse, GetPostRecordsRequest, GetPostRecordsResponse, GetPostReplyCountsRequest, GetPostReplyCountsResponse, GetProfileRecordsRequest, GetProfileRecordsResponse, GetRelationshipsRequest, GetRelationshipsResponse, GetRepostCountsRequest, GetRepostCountsResponse, GetRepostRecordsRequest, GetRepostRecordsResponse, GetRepostsByActorAndSubjectsRequest, GetRepostsByActorAndSubjectsResponse, GetRepostsBySubjectRequest, GetRepostsBySubjectResponse, GetSuggestedFeedsRequest, GetSuggestedFeedsResponse, GetThreadGateRecordsRequest, GetThreadGateRecordsResponse, GetThreadRequest, GetThreadResponse, GetTimelineRequest, GetTimelineResponse, GetUnreadNotificationCountRequest, GetUnreadNotificationCountResponse, PingRequest, PingResponse, SearchActorsRequest, SearchActorsResponse, SearchPostsRequest, SearchPostsResponse, UpdateNotificationSeenRequest, UpdateNotificationSeenResponse, UpdateTakedownRequest, UpdateTakedownResponse } from "./bsky_pb.ts"; import { MethodKind } from "@bufbuild/protobuf"; /** @@ -12,6 +12,107 @@ import { MethodKind } from "@bufbuild/protobuf"; export const Service = { typeName: "bsky.Service", methods: { + /** + * Records + * + * @generated from rpc bsky.Service.GetBlockRecords + */ + getBlockRecords: { + name: "GetBlockRecords", + I: GetBlockRecordsRequest, + O: GetBlockRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFeedGeneratorRecords + */ + getFeedGeneratorRecords: { + name: "GetFeedGeneratorRecords", + I: GetFeedGeneratorRecordsRequest, + O: GetFeedGeneratorRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetFollowRecords + */ + getFollowRecords: { + name: "GetFollowRecords", + I: GetFollowRecordsRequest, + O: GetFollowRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetLikeRecords + */ + getLikeRecords: { + name: "GetLikeRecords", + I: GetLikeRecordsRequest, + O: GetLikeRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListBlockRecords + */ + getListBlockRecords: { + name: "GetListBlockRecords", + I: GetListBlockRecordsRequest, + O: GetListBlockRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListItemRecords + */ + getListItemRecords: { + name: "GetListItemRecords", + I: GetListItemRecordsRequest, + O: GetListItemRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetListRecords + */ + getListRecords: { + name: "GetListRecords", + I: GetListRecordsRequest, + O: GetListRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetPostRecords + */ + getPostRecords: { + name: "GetPostRecords", + I: GetPostRecordsRequest, + O: GetPostRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetProfileRecords + */ + getProfileRecords: { + name: "GetProfileRecords", + I: GetProfileRecordsRequest, + O: GetProfileRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetRepostRecords + */ + getRepostRecords: { + name: "GetRepostRecords", + I: GetRepostRecordsRequest, + O: GetRepostRecordsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetThreadGateRecords + */ + getThreadGateRecords: { + name: "GetThreadGateRecords", + I: GetThreadGateRecordsRequest, + O: GetThreadGateRecordsResponse, + kind: MethodKind.Unary, + }, /** * Follows * @@ -42,21 +143,21 @@ export const Service = { kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetFollowersCount + * @generated from rpc bsky.Service.GetFollowerCounts */ - getFollowersCount: { - name: "GetFollowersCount", - I: GetFollowersCountRequest, - O: GetFollowersCountResponse, + getFollowerCounts: { + name: "GetFollowerCounts", + I: GetFollowerCountsRequest, + O: GetFollowerCountsResponse, kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetFollowsCount + * @generated from rpc bsky.Service.GetFollowCounts */ - getFollowsCount: { - name: "GetFollowsCount", - I: GetFollowsCountRequest, - O: GetFollowsCountResponse, + getFollowCounts: { + name: "GetFollowCounts", + I: GetFollowCountsRequest, + O: GetFollowCountsResponse, kind: MethodKind.Unary, }, /** @@ -71,12 +172,12 @@ export const Service = { kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetLikeByActorAndSubject + * @generated from rpc bsky.Service.GetLikesByActorAndSubjects */ - getLikeByActorAndSubject: { - name: "GetLikeByActorAndSubject", - I: GetLikeByActorAndSubjectRequest, - O: GetLikeByActorAndSubjectResponse, + getLikesByActorAndSubjects: { + name: "GetLikesByActorAndSubjects", + I: GetLikesByActorAndSubjectsRequest, + O: GetLikesByActorAndSubjectsResponse, kind: MethodKind.Unary, }, /** @@ -89,12 +190,12 @@ export const Service = { kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetLikesCount + * @generated from rpc bsky.Service.GetLikeCounts */ - getLikesCount: { - name: "GetLikesCount", - I: GetLikesCountRequest, - O: GetLikesCountResponse, + getLikeCounts: { + name: "GetLikeCounts", + I: GetLikeCountsRequest, + O: GetLikeCountsResponse, kind: MethodKind.Unary, }, /** @@ -109,12 +210,12 @@ export const Service = { kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetRepostByActorAndSubject + * @generated from rpc bsky.Service.GetRepostsByActorAndSubjects */ - getRepostByActorAndSubject: { - name: "GetRepostByActorAndSubject", - I: GetRepostByActorAndSubjectRequest, - O: GetRepostByActorAndSubjectResponse, + getRepostsByActorAndSubjects: { + name: "GetRepostsByActorAndSubjects", + I: GetRepostsByActorAndSubjectsRequest, + O: GetRepostsByActorAndSubjectsResponse, kind: MethodKind.Unary, }, /** @@ -127,32 +228,23 @@ export const Service = { kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetRepostsCount + * @generated from rpc bsky.Service.GetRepostCounts */ - getRepostsCount: { - name: "GetRepostsCount", - I: GetRepostsCountRequest, - O: GetRepostsCountResponse, + getRepostCounts: { + name: "GetRepostCounts", + I: GetRepostCountsRequest, + O: GetRepostCountsResponse, kind: MethodKind.Unary, }, /** * Profile * - * @generated from rpc bsky.Service.GetProfiles + * @generated from rpc bsky.Service.GetActors */ - getProfiles: { - name: "GetProfiles", - I: GetProfilesRequest, - O: GetProfilesResponse, - kind: MethodKind.Unary, - }, - /** - * @generated from rpc bsky.Service.GetHandles - */ - getHandles: { - name: "GetHandles", - I: GetHandlesRequest, - O: GetHandlesResponse, + getActors: { + name: "GetActors", + I: GetActorsRequest, + O: GetActorsResponse, kind: MethodKind.Unary, }, /** @@ -164,9 +256,38 @@ export const Service = { O: GetDidsByHandlesResponse, kind: MethodKind.Unary, }, + /** + * Relationships + * + * @generated from rpc bsky.Service.GetRelationships + */ + getRelationships: { + name: "GetRelationships", + I: GetRelationshipsRequest, + O: GetRelationshipsResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc bsky.Service.GetBlockExistence + */ + getBlockExistence: { + name: "GetBlockExistence", + I: GetBlockExistenceRequest, + O: GetBlockExistenceResponse, + kind: MethodKind.Unary, + }, /** * Lists * + * @generated from rpc bsky.Service.GetActorLists + */ + getActorLists: { + name: "GetActorLists", + I: GetActorListsRequest, + O: GetActorListsResponse, + kind: MethodKind.Unary, + }, + /** * @generated from rpc bsky.Service.GetListMembers */ getListMembers: { @@ -184,15 +305,6 @@ export const Service = { O: GetListMembershipResponse, kind: MethodKind.Unary, }, - /** - * @generated from rpc bsky.Service.GetList - */ - getList: { - name: "GetList", - I: GetListRequest, - O: GetListResponse, - kind: MethodKind.Unary, - }, /** * @generated from rpc bsky.Service.GetListCount */ @@ -341,15 +453,6 @@ export const Service = { /** * FeedGenerators * - * @generated from rpc bsky.Service.GetFeedGenerators - */ - getFeedGenerators: { - name: "GetFeedGenerators", - I: GetFeedGeneratorsRequest, - O: GetFeedGeneratorsResponse, - kind: MethodKind.Unary, - }, - /** * @generated from rpc bsky.Service.GetActorFeeds */ getActorFeeds: { @@ -416,15 +519,6 @@ export const Service = { O: GetThreadResponse, kind: MethodKind.Unary, }, - /** - * @generated from rpc bsky.Service.GetThreadgates - */ - getThreadgates: { - name: "GetThreadgates", - I: GetThreadgatesRequest, - O: GetThreadgatesResponse, - kind: MethodKind.Unary, - }, /** * Search * @@ -448,32 +542,32 @@ export const Service = { /** * Suggestions * - * @generated from rpc bsky.Service.GetSuggestions + * @generated from rpc bsky.Service.GetFollowSuggestions */ - getSuggestions: { - name: "GetSuggestions", - I: GetSuggestionsRequest, - O: GetSuggestionsResponse, + getFollowSuggestions: { + name: "GetFollowSuggestions", + I: GetFollowSuggestionsRequest, + O: GetFollowSuggestionsResponse, kind: MethodKind.Unary, }, /** * Posts * - * @generated from rpc bsky.Service.GetPosts + * @generated from rpc bsky.Service.GetPostReplyCounts */ - getPosts: { - name: "GetPosts", - I: GetPostsRequest, - O: GetPostsResponse, + getPostReplyCounts: { + name: "GetPostReplyCounts", + I: GetPostReplyCountsRequest, + O: GetPostReplyCountsResponse, kind: MethodKind.Unary, }, /** - * @generated from rpc bsky.Service.GetPostReplyCount + * @generated from rpc bsky.Service.GetPostCounts */ - getPostReplyCount: { - name: "GetPostReplyCount", - I: GetPostReplyCountRequest, - O: GetPostReplyCountResponse, + getPostCounts: { + name: "GetPostCounts", + I: GetPostCountsRequest, + O: GetPostCountsResponse, kind: MethodKind.Unary, }, /** diff --git a/packages/bsky/src/data-plane/gen/bsky_pb.ts b/packages/bsky/src/data-plane/gen/bsky_pb.ts index 600086aa596..ae6aae74197 100644 --- a/packages/bsky/src/data-plane/gen/bsky_pb.ts +++ b/packages/bsky/src/data-plane/gen/bsky_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v1.5.0 with parameter "target=ts" +// @generated by protoc-gen-es v1.5.0 with parameter "target=ts,import_extension=.ts" // @generated from file bsky.proto (package bsky, syntax proto3) /* eslint-disable */ // @ts-nocheck @@ -7,654 +7,1609 @@ import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialM import { Message, proto3, Timestamp } from "@bufbuild/protobuf"; /** - * - Return follow uris where user A follows users B, C, D, … - * - E.g. for viewer state on `getProfiles` - * - * @generated from message bsky.GetActorFollowsActorsRequest + * @generated from message bsky.Record */ -export class GetActorFollowsActorsRequest extends Message { +export class Record extends Message { /** - * @generated from field: string actor_did = 1; + * @generated from field: bytes record = 1; */ - actorDid = ""; + record = new Uint8Array(0); /** - * @generated from field: repeated string target_dids = 2; + * @generated from field: string cid = 2; */ - targetDids: string[] = []; + cid = ""; - constructor(data?: PartialMessage) { + /** + * @generated from field: google.protobuf.Timestamp indexed_at = 4; + */ + indexedAt?: Timestamp; + + /** + * @generated from field: bool taken_down = 5; + */ + takenDown = false; + + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetActorFollowsActorsRequest"; + static readonly typeName = "bsky.Record"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "target_dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "record", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 2, name: "cid", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "indexed_at", kind: "message", T: Timestamp }, + { no: 5, name: "taken_down", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetActorFollowsActorsRequest { - return new GetActorFollowsActorsRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): Record { + return new Record().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetActorFollowsActorsRequest { - return new GetActorFollowsActorsRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): Record { + return new Record().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetActorFollowsActorsRequest { - return new GetActorFollowsActorsRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): Record { + return new Record().fromJsonString(jsonString, options); } - static equals(a: GetActorFollowsActorsRequest | PlainMessage | undefined, b: GetActorFollowsActorsRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetActorFollowsActorsRequest, a, b); + static equals(a: Record | PlainMessage | undefined, b: Record | PlainMessage | undefined): boolean { + return proto3.util.equals(Record, a, b); } } /** - * @generated from message bsky.GetActorFollowsActorsResponse + * @generated from message bsky.GetBlockRecordsRequest */ -export class GetActorFollowsActorsResponse extends Message { +export class GetBlockRecordsRequest extends Message { /** * @generated from field: repeated string uris = 1; */ uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetActorFollowsActorsResponse"; + static readonly typeName = "bsky.GetBlockRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetActorFollowsActorsResponse { - return new GetActorFollowsActorsResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetActorFollowsActorsResponse { - return new GetActorFollowsActorsResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetActorFollowsActorsResponse { - return new GetActorFollowsActorsResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetBlockRecordsRequest { + return new GetBlockRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetActorFollowsActorsResponse | PlainMessage | undefined, b: GetActorFollowsActorsResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetActorFollowsActorsResponse, a, b); + static equals(a: GetBlockRecordsRequest | PlainMessage | undefined, b: GetBlockRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetBlockRecordsRequest, a, b); } } /** - * - Return follow uris of users who follows user A - * - For `getFollowers` list - * - * @generated from message bsky.GetFollowersRequest + * @generated from message bsky.GetBlockRecordsResponse */ -export class GetFollowersRequest extends Message { - /** - * @generated from field: string actor_did = 1; - */ - actorDid = ""; - +export class GetBlockRecordsResponse extends Message { /** - * @generated from field: int32 limit = 2; - */ - limit = 0; - - /** - * @generated from field: string cursor = 3; + * @generated from field: repeated bsky.Record records = 1; */ - cursor = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowersRequest"; + static readonly typeName = "bsky.GetBlockRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersRequest { - return new GetFollowersRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersRequest { - return new GetFollowersRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowersRequest { - return new GetFollowersRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetBlockRecordsResponse { + return new GetBlockRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetFollowersRequest | PlainMessage | undefined, b: GetFollowersRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowersRequest, a, b); + static equals(a: GetBlockRecordsResponse | PlainMessage | undefined, b: GetBlockRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetBlockRecordsResponse, a, b); } } /** - * @generated from message bsky.GetFollowersResponse + * @generated from message bsky.GetFeedGeneratorRecordsRequest */ -export class GetFollowersResponse extends Message { +export class GetFeedGeneratorRecordsRequest extends Message { /** * @generated from field: repeated string uris = 1; */ uris: string[] = []; - /** - * @generated from field: string cursor = 2; - */ - cursor = ""; - - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowersResponse"; + static readonly typeName = "bsky.GetFeedGeneratorRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersResponse { - return new GetFollowersResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersResponse { - return new GetFollowersResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowersResponse { - return new GetFollowersResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFeedGeneratorRecordsRequest { + return new GetFeedGeneratorRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetFollowersResponse | PlainMessage | undefined, b: GetFollowersResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowersResponse, a, b); + static equals(a: GetFeedGeneratorRecordsRequest | PlainMessage | undefined, b: GetFeedGeneratorRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFeedGeneratorRecordsRequest, a, b); } } /** - * - Return follow uris of users A follows - * - For `getFollows` list - * - * @generated from message bsky.GetFollowsRequest + * @generated from message bsky.GetFeedGeneratorRecordsResponse */ -export class GetFollowsRequest extends Message { - /** - * @generated from field: string actor_did = 1; - */ - actorDid = ""; - +export class GetFeedGeneratorRecordsResponse extends Message { /** - * @generated from field: int32 limit = 2; - */ - limit = 0; - - /** - * @generated from field: string cursor = 3; + * @generated from field: repeated bsky.Record records = 1; */ - cursor = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowsRequest"; + static readonly typeName = "bsky.GetFeedGeneratorRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsRequest { - return new GetFollowsRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsRequest { - return new GetFollowsRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowsRequest { - return new GetFollowsRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFeedGeneratorRecordsResponse { + return new GetFeedGeneratorRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetFollowsRequest | PlainMessage | undefined, b: GetFollowsRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowsRequest, a, b); + static equals(a: GetFeedGeneratorRecordsResponse | PlainMessage | undefined, b: GetFeedGeneratorRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFeedGeneratorRecordsResponse, a, b); } } /** - * @generated from message bsky.GetFollowsResponse + * @generated from message bsky.GetFollowRecordsRequest */ -export class GetFollowsResponse extends Message { +export class GetFollowRecordsRequest extends Message { /** * @generated from field: repeated string uris = 1; */ uris: string[] = []; - /** - * @generated from field: string cursor = 2; - */ - cursor = ""; - - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowsResponse"; + static readonly typeName = "bsky.GetFollowRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsResponse { - return new GetFollowsResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsResponse { - return new GetFollowsResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowsResponse { - return new GetFollowsResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFollowRecordsRequest { + return new GetFollowRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetFollowsResponse | PlainMessage | undefined, b: GetFollowsResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowsResponse, a, b); + static equals(a: GetFollowRecordsRequest | PlainMessage | undefined, b: GetFollowRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowRecordsRequest, a, b); } } /** - * - Return number of users who follow A - * - For `followersCount` on a profile - * - * @generated from message bsky.GetFollowersCountRequest + * @generated from message bsky.GetFollowRecordsResponse */ -export class GetFollowersCountRequest extends Message { +export class GetFollowRecordsResponse extends Message { /** - * @generated from field: string actor_did = 1; + * @generated from field: repeated bsky.Record records = 1; */ - actorDid = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowersCountRequest"; + static readonly typeName = "bsky.GetFollowRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersCountRequest { - return new GetFollowersCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersCountRequest { - return new GetFollowersCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowersCountRequest { - return new GetFollowersCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFollowRecordsResponse { + return new GetFollowRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetFollowersCountRequest | PlainMessage | undefined, b: GetFollowersCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowersCountRequest, a, b); + static equals(a: GetFollowRecordsResponse | PlainMessage | undefined, b: GetFollowRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowRecordsResponse, a, b); } } /** - * @generated from message bsky.GetFollowersCountResponse + * @generated from message bsky.GetLikeRecordsRequest */ -export class GetFollowersCountResponse extends Message { +export class GetLikeRecordsRequest extends Message { /** - * @generated from field: int32 count = 1; + * @generated from field: repeated string uris = 1; */ - count = 0; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowersCountResponse"; + static readonly typeName = "bsky.GetLikeRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersCountResponse { - return new GetFollowersCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersCountResponse { - return new GetFollowersCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowersCountResponse { - return new GetFollowersCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetLikeRecordsRequest { + return new GetLikeRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetFollowersCountResponse | PlainMessage | undefined, b: GetFollowersCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowersCountResponse, a, b); + static equals(a: GetLikeRecordsRequest | PlainMessage | undefined, b: GetLikeRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikeRecordsRequest, a, b); } } /** - * - Return number of users followed by A - * - For `followCount` on a profile - * - * @generated from message bsky.GetFollowsCountRequest + * @generated from message bsky.GetLikeRecordsResponse */ -export class GetFollowsCountRequest extends Message { +export class GetLikeRecordsResponse extends Message { /** - * @generated from field: string actor_did = 1; + * @generated from field: repeated bsky.Record records = 1; */ - actorDid = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowsCountRequest"; + static readonly typeName = "bsky.GetLikeRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsCountRequest { - return new GetFollowsCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsCountRequest { - return new GetFollowsCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowsCountRequest { - return new GetFollowsCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetLikeRecordsResponse { + return new GetLikeRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetFollowsCountRequest | PlainMessage | undefined, b: GetFollowsCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowsCountRequest, a, b); + static equals(a: GetLikeRecordsResponse | PlainMessage | undefined, b: GetLikeRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikeRecordsResponse, a, b); } } /** - * @generated from message bsky.GetFollowsCountResponse + * @generated from message bsky.GetListBlockRecordsRequest */ -export class GetFollowsCountResponse extends Message { +export class GetListBlockRecordsRequest extends Message { /** - * @generated from field: int32 count = 1; + * @generated from field: repeated string uris = 1; */ - count = 0; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFollowsCountResponse"; + static readonly typeName = "bsky.GetListBlockRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsCountResponse { - return new GetFollowsCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsCountResponse { - return new GetFollowsCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetFollowsCountResponse { - return new GetFollowsCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListBlockRecordsRequest { + return new GetListBlockRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetFollowsCountResponse | PlainMessage | undefined, b: GetFollowsCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFollowsCountResponse, a, b); + static equals(a: GetListBlockRecordsRequest | PlainMessage | undefined, b: GetListBlockRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListBlockRecordsRequest, a, b); } } /** - * - return like uris where subject uri is subject A - * - `getLikes` list for a post - * - * @generated from message bsky.GetLikesBySubjectRequest + * @generated from message bsky.GetListBlockRecordsResponse */ -export class GetLikesBySubjectRequest extends Message { - /** - * @generated from field: string subject_uri = 1; - */ - subjectUri = ""; - +export class GetListBlockRecordsResponse extends Message { /** - * @generated from field: int32 limit = 2; - */ - limit = 0; - - /** - * @generated from field: string cursor = 3; + * @generated from field: repeated bsky.Record records = 1; */ - cursor = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikesBySubjectRequest"; + static readonly typeName = "bsky.GetListBlockRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesBySubjectRequest { - return new GetLikesBySubjectRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesBySubjectRequest { - return new GetLikesBySubjectRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikesBySubjectRequest { - return new GetLikesBySubjectRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListBlockRecordsResponse { + return new GetListBlockRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetLikesBySubjectRequest | PlainMessage | undefined, b: GetLikesBySubjectRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikesBySubjectRequest, a, b); + static equals(a: GetListBlockRecordsResponse | PlainMessage | undefined, b: GetListBlockRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListBlockRecordsResponse, a, b); } } /** - * @generated from message bsky.GetLikesBySubjectResponse + * @generated from message bsky.GetListItemRecordsRequest */ -export class GetLikesBySubjectResponse extends Message { +export class GetListItemRecordsRequest extends Message { /** * @generated from field: repeated string uris = 1; */ uris: string[] = []; - /** - * @generated from field: string cursor = 2; - */ - cursor = ""; - - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikesBySubjectResponse"; + static readonly typeName = "bsky.GetListItemRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesBySubjectResponse { - return new GetLikesBySubjectResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesBySubjectResponse { - return new GetLikesBySubjectResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikesBySubjectResponse { - return new GetLikesBySubjectResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListItemRecordsRequest { + return new GetListItemRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetLikesBySubjectResponse | PlainMessage | undefined, b: GetLikesBySubjectResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikesBySubjectResponse, a, b); + static equals(a: GetListItemRecordsRequest | PlainMessage | undefined, b: GetListItemRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListItemRecordsRequest, a, b); } } /** - * - return like uri for user A on subject B - * - viewer state on posts - * - * @generated from message bsky.GetLikeByActorAndSubjectRequest + * @generated from message bsky.GetListItemRecordsResponse */ -export class GetLikeByActorAndSubjectRequest extends Message { +export class GetListItemRecordsResponse extends Message { /** - * @generated from field: string actor_did = 1; - */ - actorDid = ""; - - /** - * @generated from field: string subject_uri = 2; + * @generated from field: repeated bsky.Record records = 1; */ - subjectUri = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikeByActorAndSubjectRequest"; + static readonly typeName = "bsky.GetListItemRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeByActorAndSubjectRequest { - return new GetLikeByActorAndSubjectRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeByActorAndSubjectRequest { - return new GetLikeByActorAndSubjectRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikeByActorAndSubjectRequest { - return new GetLikeByActorAndSubjectRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListItemRecordsResponse { + return new GetListItemRecordsResponse().fromJsonString(jsonString, options); } - static equals(a: GetLikeByActorAndSubjectRequest | PlainMessage | undefined, b: GetLikeByActorAndSubjectRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikeByActorAndSubjectRequest, a, b); + static equals(a: GetListItemRecordsResponse | PlainMessage | undefined, b: GetListItemRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListItemRecordsResponse, a, b); } } /** - * @generated from message bsky.GetLikeByActorAndSubjectResponse + * @generated from message bsky.GetListRecordsRequest */ -export class GetLikeByActorAndSubjectResponse extends Message { +export class GetListRecordsRequest extends Message { /** - * @generated from field: string uri = 1; + * @generated from field: repeated string uris = 1; */ - uri = ""; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikeByActorAndSubjectResponse"; + static readonly typeName = "bsky.GetListRecordsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeByActorAndSubjectResponse { - return new GetLikeByActorAndSubjectResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListRecordsRequest { + return new GetListRecordsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeByActorAndSubjectResponse { - return new GetLikeByActorAndSubjectResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListRecordsRequest { + return new GetListRecordsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikeByActorAndSubjectResponse { - return new GetLikeByActorAndSubjectResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListRecordsRequest { + return new GetListRecordsRequest().fromJsonString(jsonString, options); } - static equals(a: GetLikeByActorAndSubjectResponse | PlainMessage | undefined, b: GetLikeByActorAndSubjectResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikeByActorAndSubjectResponse, a, b); + static equals(a: GetListRecordsRequest | PlainMessage | undefined, b: GetListRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListRecordsRequest, a, b); } } /** - * - return recent like uris for user A - * - `getActorLikes` list for a user - * - * @generated from message bsky.GetActorLikesRequest + * @generated from message bsky.GetListRecordsResponse */ -export class GetActorLikesRequest extends Message { - /** - * @generated from field: string actor_did = 1; - */ - actorDid = ""; - - /** - * @generated from field: int32 limit = 2; - */ - limit = 0; - +export class GetListRecordsResponse extends Message { /** - * @generated from field: string cursor = 3; + * @generated from field: repeated bsky.Record records = 1; */ - cursor = ""; + records: Record[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetActorLikesRequest"; + static readonly typeName = "bsky.GetListRecordsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetActorLikesRequest { - return new GetActorLikesRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListRecordsResponse { + return new GetListRecordsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetActorLikesRequest { - return new GetActorLikesRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListRecordsResponse { + return new GetListRecordsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetActorLikesRequest { + static fromJsonString(jsonString: string, options?: Partial): GetListRecordsResponse { + return new GetListRecordsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetListRecordsResponse | PlainMessage | undefined, b: GetListRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListRecordsResponse, a, b); + } +} + +/** + * @generated from message bsky.PostRecordMeta + */ +export class PostRecordMeta extends Message { + /** + * @generated from field: bool violates_thread_gate = 1; + */ + violatesThreadGate = false; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.PostRecordMeta"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "violates_thread_gate", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): PostRecordMeta { + return new PostRecordMeta().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): PostRecordMeta { + return new PostRecordMeta().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): PostRecordMeta { + return new PostRecordMeta().fromJsonString(jsonString, options); + } + + static equals(a: PostRecordMeta | PlainMessage | undefined, b: PostRecordMeta | PlainMessage | undefined): boolean { + return proto3.util.equals(PostRecordMeta, a, b); + } +} + +/** + * @generated from message bsky.GetPostRecordsRequest + */ +export class GetPostRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetPostRecordsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetPostRecordsRequest { + return new GetPostRecordsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetPostRecordsRequest | PlainMessage | undefined, b: GetPostRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostRecordsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetPostRecordsResponse + */ +export class GetPostRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = []; + + /** + * @generated from field: repeated bsky.PostRecordMeta meta = 2; + */ + meta: PostRecordMeta[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetPostRecordsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, + { no: 2, name: "meta", kind: "message", T: PostRecordMeta, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetPostRecordsResponse { + return new GetPostRecordsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetPostRecordsResponse | PlainMessage | undefined, b: GetPostRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostRecordsResponse, a, b); + } +} + +/** + * @generated from message bsky.GetProfileRecordsRequest + */ +export class GetProfileRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetProfileRecordsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetProfileRecordsRequest { + return new GetProfileRecordsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetProfileRecordsRequest | PlainMessage | undefined, b: GetProfileRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetProfileRecordsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetProfileRecordsResponse + */ +export class GetProfileRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetProfileRecordsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetProfileRecordsResponse { + return new GetProfileRecordsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetProfileRecordsResponse | PlainMessage | undefined, b: GetProfileRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetProfileRecordsResponse, a, b); + } +} + +/** + * @generated from message bsky.GetRepostRecordsRequest + */ +export class GetRepostRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetRepostRecordsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetRepostRecordsRequest { + return new GetRepostRecordsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetRepostRecordsRequest | PlainMessage | undefined, b: GetRepostRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostRecordsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetRepostRecordsResponse + */ +export class GetRepostRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetRepostRecordsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetRepostRecordsResponse { + return new GetRepostRecordsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetRepostRecordsResponse | PlainMessage | undefined, b: GetRepostRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostRecordsResponse, a, b); + } +} + +/** + * @generated from message bsky.GetThreadGateRecordsRequest + */ +export class GetThreadGateRecordsRequest extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetThreadGateRecordsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetThreadGateRecordsRequest { + return new GetThreadGateRecordsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetThreadGateRecordsRequest | PlainMessage | undefined, b: GetThreadGateRecordsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetThreadGateRecordsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetThreadGateRecordsResponse + */ +export class GetThreadGateRecordsResponse extends Message { + /** + * @generated from field: repeated bsky.Record records = 1; + */ + records: Record[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetThreadGateRecordsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "records", kind: "message", T: Record, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetThreadGateRecordsResponse { + return new GetThreadGateRecordsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetThreadGateRecordsResponse | PlainMessage | undefined, b: GetThreadGateRecordsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetThreadGateRecordsResponse, a, b); + } +} + +/** + * - Return follow uris where user A follows users B, C, D, … + * - E.g. for viewer state on `getProfiles` + * + * @generated from message bsky.GetActorFollowsActorsRequest + */ +export class GetActorFollowsActorsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: repeated string target_dids = 2; + */ + targetDids: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetActorFollowsActorsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "target_dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetActorFollowsActorsRequest { + return new GetActorFollowsActorsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetActorFollowsActorsRequest | PlainMessage | undefined, b: GetActorFollowsActorsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorFollowsActorsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetActorFollowsActorsResponse + */ +export class GetActorFollowsActorsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetActorFollowsActorsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetActorFollowsActorsResponse { + return new GetActorFollowsActorsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetActorFollowsActorsResponse | PlainMessage | undefined, b: GetActorFollowsActorsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorFollowsActorsResponse, a, b); + } +} + +/** + * - Return follow uris of users who follows user A + * - For `getFollowers` list + * + * @generated from message bsky.GetFollowersRequest + */ +export class GetFollowersRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0; + + /** + * @generated from field: string cursor = 3; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowersRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersRequest { + return new GetFollowersRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersRequest { + return new GetFollowersRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowersRequest { + return new GetFollowersRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowersRequest | PlainMessage | undefined, b: GetFollowersRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowersRequest, a, b); + } +} + +/** + * @generated from message bsky.FollowInfo + */ +export class FollowInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = ""; + + /** + * @generated from field: string did = 2; + */ + did = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.FollowInfo"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): FollowInfo { + return new FollowInfo().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): FollowInfo { + return new FollowInfo().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): FollowInfo { + return new FollowInfo().fromJsonString(jsonString, options); + } + + static equals(a: FollowInfo | PlainMessage | undefined, b: FollowInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(FollowInfo, a, b); + } +} + +/** + * @generated from message bsky.GetFollowersResponse + */ +export class GetFollowersResponse extends Message { + /** + * @generated from field: repeated bsky.FollowInfo followers = 1; + */ + followers: FollowInfo[] = []; + + /** + * @generated from field: string cursor = 2; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowersResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "followers", kind: "message", T: FollowInfo, repeated: true }, + { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowersResponse { + return new GetFollowersResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowersResponse { + return new GetFollowersResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowersResponse { + return new GetFollowersResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowersResponse | PlainMessage | undefined, b: GetFollowersResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowersResponse, a, b); + } +} + +/** + * - Return follow uris of users A follows + * - For `getFollows` list + * + * @generated from message bsky.GetFollowsRequest + */ +export class GetFollowsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0; + + /** + * @generated from field: string cursor = 3; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsRequest { + return new GetFollowsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsRequest { + return new GetFollowsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowsRequest { + return new GetFollowsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowsRequest | PlainMessage | undefined, b: GetFollowsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetFollowsResponse + */ +export class GetFollowsResponse extends Message { + /** + * @generated from field: repeated bsky.FollowInfo follows = 1; + */ + follows: FollowInfo[] = []; + + /** + * @generated from field: string cursor = 2; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "follows", kind: "message", T: FollowInfo, repeated: true }, + { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowsResponse { + return new GetFollowsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowsResponse { + return new GetFollowsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowsResponse { + return new GetFollowsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowsResponse | PlainMessage | undefined, b: GetFollowsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowsResponse, a, b); + } +} + +/** + * - Return number of users who follow A + * - For `followersCount` on a profile + * + * @generated from message bsky.GetFollowerCountsRequest + */ +export class GetFollowerCountsRequest extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowerCountsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowerCountsRequest { + return new GetFollowerCountsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowerCountsRequest { + return new GetFollowerCountsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowerCountsRequest { + return new GetFollowerCountsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowerCountsRequest | PlainMessage | undefined, b: GetFollowerCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowerCountsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetFollowerCountsResponse + */ +export class GetFollowerCountsResponse extends Message { + /** + * @generated from field: repeated int32 counts = 1; + */ + counts: number[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowerCountsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowerCountsResponse { + return new GetFollowerCountsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowerCountsResponse { + return new GetFollowerCountsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowerCountsResponse { + return new GetFollowerCountsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowerCountsResponse | PlainMessage | undefined, b: GetFollowerCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowerCountsResponse, a, b); + } +} + +/** + * - Return number of users followed by A + * - For `followCount` on a profile + * + * @generated from message bsky.GetFollowCountsRequest + */ +export class GetFollowCountsRequest extends Message { + /** + * @generated from field: repeated string dids = 1; + */ + dids: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowCountsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowCountsRequest { + return new GetFollowCountsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowCountsRequest { + return new GetFollowCountsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowCountsRequest { + return new GetFollowCountsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowCountsRequest | PlainMessage | undefined, b: GetFollowCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowCountsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetFollowCountsResponse + */ +export class GetFollowCountsResponse extends Message { + /** + * @generated from field: repeated int32 counts = 1; + */ + counts: number[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetFollowCountsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowCountsResponse { + return new GetFollowCountsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowCountsResponse { + return new GetFollowCountsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetFollowCountsResponse { + return new GetFollowCountsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetFollowCountsResponse | PlainMessage | undefined, b: GetFollowCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowCountsResponse, a, b); + } +} + +/** + * - return like uris where subject uri is subject A + * - `getLikes` list for a post + * + * @generated from message bsky.GetLikesBySubjectRequest + */ +export class GetLikesBySubjectRequest extends Message { + /** + * @generated from field: bsky.RecordRef subject = 1; + */ + subject?: RecordRef; + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0; + + /** + * @generated from field: string cursor = 3; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetLikesBySubjectRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "subject", kind: "message", T: RecordRef }, + { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetLikesBySubjectRequest { + return new GetLikesBySubjectRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetLikesBySubjectRequest | PlainMessage | undefined, b: GetLikesBySubjectRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikesBySubjectRequest, a, b); + } +} + +/** + * @generated from message bsky.GetLikesBySubjectResponse + */ +export class GetLikesBySubjectResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + /** + * @generated from field: string cursor = 2; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetLikesBySubjectResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetLikesBySubjectResponse { + return new GetLikesBySubjectResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetLikesBySubjectResponse | PlainMessage | undefined, b: GetLikesBySubjectResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikesBySubjectResponse, a, b); + } +} + +/** + * - return like uris for user A on subject B, C, D... + * - viewer state on posts + * + * @generated from message bsky.GetLikesByActorAndSubjectsRequest + */ +export class GetLikesByActorAndSubjectsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: repeated bsky.RecordRef refs = 2; + */ + refs: RecordRef[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetLikesByActorAndSubjectsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "refs", kind: "message", T: RecordRef, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetLikesByActorAndSubjectsRequest { + return new GetLikesByActorAndSubjectsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetLikesByActorAndSubjectsRequest | PlainMessage | undefined, b: GetLikesByActorAndSubjectsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikesByActorAndSubjectsRequest, a, b); + } +} + +/** + * @generated from message bsky.GetLikesByActorAndSubjectsResponse + */ +export class GetLikesByActorAndSubjectsResponse extends Message { + /** + * @generated from field: repeated string uris = 1; + */ + uris: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetLikesByActorAndSubjectsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetLikesByActorAndSubjectsResponse { + return new GetLikesByActorAndSubjectsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetLikesByActorAndSubjectsResponse | PlainMessage | undefined, b: GetLikesByActorAndSubjectsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikesByActorAndSubjectsResponse, a, b); + } +} + +/** + * - return recent like uris for user A + * - `getActorLikes` list for a user + * + * @generated from message bsky.GetActorLikesRequest + */ +export class GetActorLikesRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: int32 limit = 2; + */ + limit = 0; + + /** + * @generated from field: string cursor = 3; + */ + cursor = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetActorLikesRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorLikesRequest { + return new GetActorLikesRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorLikesRequest { + return new GetActorLikesRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetActorLikesRequest { return new GetActorLikesRequest().fromJsonString(jsonString, options); } @@ -663,14 +1618,57 @@ export class GetActorLikesRequest extends Message { } } +/** + * @generated from message bsky.LikeInfo + */ +export class LikeInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = ""; + + /** + * @generated from field: string subject = 2; + */ + subject = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.LikeInfo"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "subject", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): LikeInfo { + return new LikeInfo().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): LikeInfo { + return new LikeInfo().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): LikeInfo { + return new LikeInfo().fromJsonString(jsonString, options); + } + + static equals(a: LikeInfo | PlainMessage | undefined, b: LikeInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(LikeInfo, a, b); + } +} + /** * @generated from message bsky.GetActorLikesResponse */ export class GetActorLikesResponse extends Message { /** - * @generated from field: repeated string uris = 1; + * @generated from field: repeated bsky.LikeInfo likes = 1; */ - uris: string[] = []; + likes: LikeInfo[] = []; /** * @generated from field: string cursor = 2; @@ -685,7 +1683,7 @@ export class GetActorLikesResponse extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "bsky.GetActorLikesResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "likes", kind: "message", T: LikeInfo, repeated: true }, { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); @@ -707,79 +1705,79 @@ export class GetActorLikesResponse extends Message { } /** - * - return number of likes on subject A + * - return number of likes on subjects A, B, C... * - post or feed generator hydration `likeCount` field * - * @generated from message bsky.GetLikesCountRequest + * @generated from message bsky.GetLikeCountsRequest */ -export class GetLikesCountRequest extends Message { +export class GetLikeCountsRequest extends Message { /** - * @generated from field: string subject_uri = 1; + * @generated from field: repeated string uris = 1; */ - subjectUri = ""; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikesCountRequest"; + static readonly typeName = "bsky.GetLikeCountsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesCountRequest { - return new GetLikesCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeCountsRequest { + return new GetLikeCountsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesCountRequest { - return new GetLikesCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeCountsRequest { + return new GetLikeCountsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikesCountRequest { - return new GetLikesCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetLikeCountsRequest { + return new GetLikeCountsRequest().fromJsonString(jsonString, options); } - static equals(a: GetLikesCountRequest | PlainMessage | undefined, b: GetLikesCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikesCountRequest, a, b); + static equals(a: GetLikeCountsRequest | PlainMessage | undefined, b: GetLikeCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikeCountsRequest, a, b); } } /** - * @generated from message bsky.GetLikesCountResponse + * @generated from message bsky.GetLikeCountsResponse */ -export class GetLikesCountResponse extends Message { +export class GetLikeCountsResponse extends Message { /** - * @generated from field: int32 count = 1; + * @generated from field: repeated int32 counts = 1; */ - count = 0; + counts: number[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetLikesCountResponse"; + static readonly typeName = "bsky.GetLikeCountsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetLikesCountResponse { - return new GetLikesCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetLikeCountsResponse { + return new GetLikeCountsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetLikesCountResponse { - return new GetLikesCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetLikeCountsResponse { + return new GetLikeCountsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetLikesCountResponse { - return new GetLikesCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetLikeCountsResponse { + return new GetLikeCountsResponse().fromJsonString(jsonString, options); } - static equals(a: GetLikesCountResponse | PlainMessage | undefined, b: GetLikesCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetLikesCountResponse, a, b); + static equals(a: GetLikeCountsResponse | PlainMessage | undefined, b: GetLikeCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetLikeCountsResponse, a, b); } } @@ -796,12 +1794,17 @@ export class GetRepostsBySubjectRequest extends Message [ { no: 1, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "subject_cid", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, + { no: 3, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 4, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostsBySubjectRequest { @@ -879,85 +1883,128 @@ export class GetRepostsBySubjectResponse extends Message { +export class GetRepostsByActorAndSubjectsRequest extends Message { /** * @generated from field: string actor_did = 1; */ actorDid = ""; /** - * @generated from field: string subject_uri = 2; + * @generated from field: repeated bsky.RecordRef refs = 2; */ - subjectUri = ""; + refs: RecordRef[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetRepostByActorAndSubjectRequest"; + static readonly typeName = "bsky.GetRepostsByActorAndSubjectsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "refs", kind: "message", T: RecordRef, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetRepostsByActorAndSubjectsRequest { + return new GetRepostsByActorAndSubjectsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetRepostsByActorAndSubjectsRequest | PlainMessage | undefined, b: GetRepostsByActorAndSubjectsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostsByActorAndSubjectsRequest, a, b); + } +} + +/** + * @generated from message bsky.RecordRef + */ +export class RecordRef extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = ""; + + /** + * @generated from field: string cid = 2; + */ + cid = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.RecordRef"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "cid", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostByActorAndSubjectRequest { - return new GetRepostByActorAndSubjectRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): RecordRef { + return new RecordRef().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostByActorAndSubjectRequest { - return new GetRepostByActorAndSubjectRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): RecordRef { + return new RecordRef().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetRepostByActorAndSubjectRequest { - return new GetRepostByActorAndSubjectRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): RecordRef { + return new RecordRef().fromJsonString(jsonString, options); } - static equals(a: GetRepostByActorAndSubjectRequest | PlainMessage | undefined, b: GetRepostByActorAndSubjectRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetRepostByActorAndSubjectRequest, a, b); + static equals(a: RecordRef | PlainMessage | undefined, b: RecordRef | PlainMessage | undefined): boolean { + return proto3.util.equals(RecordRef, a, b); } } /** - * @generated from message bsky.GetRepostByActorAndSubjectResponse + * @generated from message bsky.GetRepostsByActorAndSubjectsResponse */ -export class GetRepostByActorAndSubjectResponse extends Message { +export class GetRepostsByActorAndSubjectsResponse extends Message { /** - * @generated from field: string uri = 1; + * @generated from field: repeated string uris = 1; */ - uri = ""; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetRepostByActorAndSubjectResponse"; + static readonly typeName = "bsky.GetRepostsByActorAndSubjectsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostByActorAndSubjectResponse { - return new GetRepostByActorAndSubjectResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostByActorAndSubjectResponse { - return new GetRepostByActorAndSubjectResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetRepostByActorAndSubjectResponse { - return new GetRepostByActorAndSubjectResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetRepostsByActorAndSubjectsResponse { + return new GetRepostsByActorAndSubjectsResponse().fromJsonString(jsonString, options); } - static equals(a: GetRepostByActorAndSubjectResponse | PlainMessage | undefined, b: GetRepostByActorAndSubjectResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetRepostByActorAndSubjectResponse, a, b); + static equals(a: GetRepostsByActorAndSubjectsResponse | PlainMessage | undefined, b: GetRepostsByActorAndSubjectsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostsByActorAndSubjectsResponse, a, b); } } @@ -1060,231 +2107,209 @@ export class GetActorRepostsResponse extends Message { * - return number of reposts on subject A * - post or feed generator hydration `repostCount` field * - * @generated from message bsky.GetRepostsCountRequest + * @generated from message bsky.GetRepostCountsRequest */ -export class GetRepostsCountRequest extends Message { +export class GetRepostCountsRequest extends Message { /** - * @generated from field: string subject_uri = 1; + * @generated from field: repeated string uris = 1; */ - subjectUri = ""; + uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetRepostsCountRequest"; + static readonly typeName = "bsky.GetRepostCountsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "subject_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostsCountRequest { - return new GetRepostsCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostCountsRequest { + return new GetRepostCountsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostsCountRequest { - return new GetRepostsCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostCountsRequest { + return new GetRepostCountsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetRepostsCountRequest { - return new GetRepostsCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetRepostCountsRequest { + return new GetRepostCountsRequest().fromJsonString(jsonString, options); } - static equals(a: GetRepostsCountRequest | PlainMessage | undefined, b: GetRepostsCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetRepostsCountRequest, a, b); + static equals(a: GetRepostCountsRequest | PlainMessage | undefined, b: GetRepostCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostCountsRequest, a, b); } } /** - * @generated from message bsky.GetRepostsCountResponse + * @generated from message bsky.GetRepostCountsResponse */ -export class GetRepostsCountResponse extends Message { +export class GetRepostCountsResponse extends Message { /** - * @generated from field: int32 count = 1; + * @generated from field: repeated int32 counts = 1; */ - count = 0; + counts: number[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetRepostsCountResponse"; + static readonly typeName = "bsky.GetRepostCountsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostsCountResponse { - return new GetRepostsCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetRepostCountsResponse { + return new GetRepostCountsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostsCountResponse { - return new GetRepostsCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetRepostCountsResponse { + return new GetRepostCountsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetRepostsCountResponse { - return new GetRepostsCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetRepostCountsResponse { + return new GetRepostCountsResponse().fromJsonString(jsonString, options); } - static equals(a: GetRepostsCountResponse | PlainMessage | undefined, b: GetRepostsCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetRepostsCountResponse, a, b); + static equals(a: GetRepostCountsResponse | PlainMessage | undefined, b: GetRepostCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRepostCountsResponse, a, b); } } /** - * - return profile record for dids A, B, C… + * - return actor information for dids A, B, C… * - profile hydration * - should this include handles? apply repo takedown? * - * @generated from message bsky.GetProfilesRequest + * @generated from message bsky.GetActorsRequest */ -export class GetProfilesRequest extends Message { +export class GetActorsRequest extends Message { /** * @generated from field: repeated string dids = 1; */ dids: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetProfilesRequest"; + static readonly typeName = "bsky.GetActorsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetProfilesRequest { - return new GetProfilesRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorsRequest { + return new GetActorsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetProfilesRequest { - return new GetProfilesRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorsRequest { + return new GetActorsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetProfilesRequest { - return new GetProfilesRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetActorsRequest { + return new GetActorsRequest().fromJsonString(jsonString, options); } - static equals(a: GetProfilesRequest | PlainMessage | undefined, b: GetProfilesRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetProfilesRequest, a, b); + static equals(a: GetActorsRequest | PlainMessage | undefined, b: GetActorsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorsRequest, a, b); } } /** - * @generated from message bsky.GetProfilesResponse + * @generated from message bsky.ActorInfo */ -export class GetProfilesResponse extends Message { +export class ActorInfo extends Message { /** - * @generated from field: repeated bytes records = 1; + * @generated from field: bool exists = 1; */ - records: Uint8Array[] = []; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetProfilesResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "records", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetProfilesResponse { - return new GetProfilesResponse().fromBinary(bytes, options); - } + exists = false; - static fromJson(jsonValue: JsonValue, options?: Partial): GetProfilesResponse { - return new GetProfilesResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetProfilesResponse { - return new GetProfilesResponse().fromJsonString(jsonString, options); - } + /** + * @generated from field: string handle = 2; + */ + handle = ""; - static equals(a: GetProfilesResponse | PlainMessage | undefined, b: GetProfilesResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetProfilesResponse, a, b); - } -} + /** + * @generated from field: bsky.Record profile = 3; + */ + profile?: Record; -/** - * - return handle for dids A, B, C… - * - profile hydration - * - * @generated from message bsky.GetHandlesRequest - */ -export class GetHandlesRequest extends Message { /** - * @generated from field: repeated string dids = 1; + * @generated from field: bool taken_down = 4; */ - dids: string[] = []; + takenDown = false; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetHandlesRequest"; + static readonly typeName = "bsky.ActorInfo"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "exists", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 2, name: "handle", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "profile", kind: "message", T: Record }, + { no: 4, name: "taken_down", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetHandlesRequest { - return new GetHandlesRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): ActorInfo { + return new ActorInfo().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetHandlesRequest { - return new GetHandlesRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): ActorInfo { + return new ActorInfo().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetHandlesRequest { - return new GetHandlesRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): ActorInfo { + return new ActorInfo().fromJsonString(jsonString, options); } - static equals(a: GetHandlesRequest | PlainMessage | undefined, b: GetHandlesRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetHandlesRequest, a, b); + static equals(a: ActorInfo | PlainMessage | undefined, b: ActorInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(ActorInfo, a, b); } } /** - * @generated from message bsky.GetHandlesResponse + * @generated from message bsky.GetActorsResponse */ -export class GetHandlesResponse extends Message { +export class GetActorsResponse extends Message { /** - * @generated from field: repeated string handles = 1; + * @generated from field: repeated bsky.ActorInfo actors = 1; */ - handles: string[] = []; + actors: ActorInfo[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetHandlesResponse"; + static readonly typeName = "bsky.GetActorsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "handles", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "actors", kind: "message", T: ActorInfo, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetHandlesResponse { - return new GetHandlesResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorsResponse { + return new GetActorsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetHandlesResponse { - return new GetHandlesResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorsResponse { + return new GetActorsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetHandlesResponse { - return new GetHandlesResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetActorsResponse { + return new GetActorsResponse().fromJsonString(jsonString, options); } - static equals(a: GetHandlesResponse | PlainMessage | undefined, b: GetHandlesResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetHandlesResponse, a, b); + static equals(a: GetActorsResponse | PlainMessage | undefined, b: GetActorsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorsResponse, a, b); } } @@ -1349,20 +2374,346 @@ export class GetDidsByHandlesResponse extends Message { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetDidsByHandlesResponse { - return new GetDidsByHandlesResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetDidsByHandlesResponse { + return new GetDidsByHandlesResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetDidsByHandlesResponse | PlainMessage | undefined, b: GetDidsByHandlesResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetDidsByHandlesResponse, a, b); + } +} + +/** + * - return relationships between user A and users B, C, D... + * - profile hydration + * - block application + * + * @generated from message bsky.GetRelationshipsRequest + */ +export class GetRelationshipsRequest extends Message { + /** + * @generated from field: string actor_did = 1; + */ + actorDid = ""; + + /** + * @generated from field: repeated string target_dids = 2; + */ + targetDids: string[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetRelationshipsRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "target_dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetRelationshipsRequest { + return new GetRelationshipsRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetRelationshipsRequest | PlainMessage | undefined, b: GetRelationshipsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRelationshipsRequest, a, b); + } +} + +/** + * @generated from message bsky.Relationships + */ +export class Relationships extends Message { + /** + * @generated from field: bool muted = 1; + */ + muted = false; + + /** + * @generated from field: string muted_by_list = 2; + */ + mutedByList = ""; + + /** + * @generated from field: string blocked_by = 3; + */ + blockedBy = ""; + + /** + * @generated from field: string blocking = 4; + */ + blocking = ""; + + /** + * @generated from field: string blocked_by_list = 5; + */ + blockedByList = ""; + + /** + * @generated from field: string blocking_by_list = 6; + */ + blockingByList = ""; + + /** + * @generated from field: string following = 7; + */ + following = ""; + + /** + * @generated from field: string followed_by = 8; + */ + followedBy = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.Relationships"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "muted", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 2, name: "muted_by_list", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "blocked_by", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "blocking", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 5, name: "blocked_by_list", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 6, name: "blocking_by_list", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 7, name: "following", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 8, name: "followed_by", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): Relationships { + return new Relationships().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): Relationships { + return new Relationships().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): Relationships { + return new Relationships().fromJsonString(jsonString, options); + } + + static equals(a: Relationships | PlainMessage | undefined, b: Relationships | PlainMessage | undefined): boolean { + return proto3.util.equals(Relationships, a, b); + } +} + +/** + * @generated from message bsky.GetRelationshipsResponse + */ +export class GetRelationshipsResponse extends Message { + /** + * @generated from field: repeated bsky.Relationships relationships = 1; + */ + relationships: Relationships[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetRelationshipsResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "relationships", kind: "message", T: Relationships, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetRelationshipsResponse { + return new GetRelationshipsResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetRelationshipsResponse | PlainMessage | undefined, b: GetRelationshipsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetRelationshipsResponse, a, b); + } +} + +/** + * - return whether a block (bidrectionally and either direct or through a list) exists between two dids + * - enforcing 3rd party block violations + * + * @generated from message bsky.RelationshipPair + */ +export class RelationshipPair extends Message { + /** + * @generated from field: string a = 1; + */ + a = ""; + + /** + * @generated from field: string b = 2; + */ + b = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.RelationshipPair"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "a", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "b", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): RelationshipPair { + return new RelationshipPair().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): RelationshipPair { + return new RelationshipPair().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): RelationshipPair { + return new RelationshipPair().fromJsonString(jsonString, options); + } + + static equals(a: RelationshipPair | PlainMessage | undefined, b: RelationshipPair | PlainMessage | undefined): boolean { + return proto3.util.equals(RelationshipPair, a, b); + } +} + +/** + * @generated from message bsky.GetBlockExistenceRequest + */ +export class GetBlockExistenceRequest extends Message { + /** + * @generated from field: repeated bsky.RelationshipPair pairs = 1; + */ + pairs: RelationshipPair[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetBlockExistenceRequest"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "pairs", kind: "message", T: RelationshipPair, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetBlockExistenceRequest { + return new GetBlockExistenceRequest().fromJsonString(jsonString, options); + } + + static equals(a: GetBlockExistenceRequest | PlainMessage | undefined, b: GetBlockExistenceRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetBlockExistenceRequest, a, b); + } +} + +/** + * @generated from message bsky.GetBlockExistenceResponse + */ +export class GetBlockExistenceResponse extends Message { + /** + * @generated from field: repeated bool exists = 1; + */ + exists: boolean[] = []; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.GetBlockExistenceResponse"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "exists", kind: "scalar", T: 8 /* ScalarType.BOOL */, repeated: true }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): GetBlockExistenceResponse { + return new GetBlockExistenceResponse().fromJsonString(jsonString, options); + } + + static equals(a: GetBlockExistenceResponse | PlainMessage | undefined, b: GetBlockExistenceResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetBlockExistenceResponse, a, b); + } +} + +/** + * @generated from message bsky.ListItemInfo + */ +export class ListItemInfo extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = ""; + + /** + * @generated from field: string did = 2; + */ + did = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.ListItemInfo"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): ListItemInfo { + return new ListItemInfo().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetDidsByHandlesResponse { - return new GetDidsByHandlesResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): ListItemInfo { + return new ListItemInfo().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetDidsByHandlesResponse { - return new GetDidsByHandlesResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): ListItemInfo { + return new ListItemInfo().fromJsonString(jsonString, options); } - static equals(a: GetDidsByHandlesResponse | PlainMessage | undefined, b: GetDidsByHandlesResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetDidsByHandlesResponse, a, b); + static equals(a: ListItemInfo | PlainMessage | undefined, b: ListItemInfo | PlainMessage | undefined): boolean { + return proto3.util.equals(ListItemInfo, a, b); } } @@ -1423,9 +2774,9 @@ export class GetListMembersRequest extends Message { */ export class GetListMembersResponse extends Message { /** - * @generated from field: repeated string dids = 1; + * @generated from field: repeated bsky.ListItemInfo listitems = 1; */ - dids: string[] = []; + listitems: ListItemInfo[] = []; /** * @generated from field: string cursor = 2; @@ -1440,7 +2791,7 @@ export class GetListMembersResponse extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "bsky.GetListMembersResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "listitems", kind: "message", T: ListItemInfo, repeated: true }, { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); @@ -1545,156 +2896,174 @@ export class GetListMembershipResponse extends Message { +export class GetListCountRequest extends Message { /** * @generated from field: string list_uri = 1; */ listUri = ""; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetListRequest"; + static readonly typeName = "bsky.GetListCountRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "list_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetListRequest { - return new GetListRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListCountRequest { + return new GetListCountRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetListRequest { - return new GetListRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListCountRequest { + return new GetListCountRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetListRequest { - return new GetListRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListCountRequest { + return new GetListCountRequest().fromJsonString(jsonString, options); } - static equals(a: GetListRequest | PlainMessage | undefined, b: GetListRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetListRequest, a, b); + static equals(a: GetListCountRequest | PlainMessage | undefined, b: GetListCountRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListCountRequest, a, b); } } /** - * @generated from message bsky.GetListResponse + * @generated from message bsky.GetListCountResponse */ -export class GetListResponse extends Message { +export class GetListCountResponse extends Message { /** - * @generated from field: bytes record = 1; + * @generated from field: int32 count = 1; */ - record = new Uint8Array(0); + count = 0; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetListResponse"; + static readonly typeName = "bsky.GetListCountResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "record", kind: "scalar", T: 12 /* ScalarType.BYTES */ }, + { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetListResponse { - return new GetListResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetListCountResponse { + return new GetListCountResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetListResponse { - return new GetListResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetListCountResponse { + return new GetListCountResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetListResponse { - return new GetListResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetListCountResponse { + return new GetListCountResponse().fromJsonString(jsonString, options); } - static equals(a: GetListResponse | PlainMessage | undefined, b: GetListResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetListResponse, a, b); + static equals(a: GetListCountResponse | PlainMessage | undefined, b: GetListCountResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetListCountResponse, a, b); } } /** - * - Return number of items in list A - * - For aggregate + * - return list of uris of lists created by A + * - `getLists` * - * @generated from message bsky.GetListCountRequest + * @generated from message bsky.GetActorListsRequest */ -export class GetListCountRequest extends Message { +export class GetActorListsRequest extends Message { /** - * @generated from field: string list_uri = 1; + * @generated from field: string actor_did = 1; */ - listUri = ""; + actorDid = ""; - constructor(data?: PartialMessage) { + /** + * @generated from field: int32 limit = 2; + */ + limit = 0; + + /** + * @generated from field: string cursor = 3; + */ + cursor = ""; + + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetListCountRequest"; + static readonly typeName = "bsky.GetActorListsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "list_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetListCountRequest { - return new GetListCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorListsRequest { + return new GetActorListsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetListCountRequest { - return new GetListCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorListsRequest { + return new GetActorListsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetListCountRequest { - return new GetListCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetActorListsRequest { + return new GetActorListsRequest().fromJsonString(jsonString, options); } - static equals(a: GetListCountRequest | PlainMessage | undefined, b: GetListCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetListCountRequest, a, b); + static equals(a: GetActorListsRequest | PlainMessage | undefined, b: GetActorListsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorListsRequest, a, b); } } /** - * @generated from message bsky.GetListCountResponse + * @generated from message bsky.GetActorListsResponse */ -export class GetListCountResponse extends Message { +export class GetActorListsResponse extends Message { /** - * @generated from field: int32 count = 1; + * @generated from field: repeated string list_uris = 1; */ - count = 0; + listUris: string[] = []; - constructor(data?: PartialMessage) { + /** + * @generated from field: string cursor = 2; + */ + cursor = ""; + + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetListCountResponse"; + static readonly typeName = "bsky.GetActorListsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "count", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 1, name: "list_uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetListCountResponse { - return new GetListCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetActorListsResponse { + return new GetActorListsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetListCountResponse { - return new GetListCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetActorListsResponse { + return new GetActorListsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetListCountResponse { - return new GetListCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetActorListsResponse { + return new GetActorListsResponse().fromJsonString(jsonString, options); } - static equals(a: GetListCountResponse | PlainMessage | undefined, b: GetListCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetListCountResponse, a, b); + static equals(a: GetActorListsResponse | PlainMessage | undefined, b: GetActorListsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetActorListsResponse, a, b); } } @@ -2452,9 +3821,9 @@ export class GetBlocklistSubscriptionRequest extends Message { /** - * @generated from field: bool subscribed = 1; + * @generated from field: string listblock_uri = 1; */ - subscribed = false; + listblockUri = ""; constructor(data?: PartialMessage) { super(); @@ -2464,7 +3833,7 @@ export class GetBlocklistSubscriptionResponse extends Message [ - { no: 1, name: "subscribed", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 1, name: "listblock_uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): GetBlocklistSubscriptionResponse { @@ -2647,7 +4016,12 @@ export class Notification extends Message { reason = ""; /** - * @generated from field: google.protobuf.Timestamp timestamp = 3; + * @generated from field: string reason_subject = 3; + */ + reasonSubject = ""; + + /** + * @generated from field: google.protobuf.Timestamp timestamp = 4; */ timestamp?: Timestamp; @@ -2661,7 +4035,8 @@ export class Notification extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "reason", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "timestamp", kind: "message", T: Timestamp }, + { no: 3, name: "reason_subject", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "timestamp", kind: "message", T: Timestamp }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Notification { @@ -2955,83 +4330,6 @@ export class GetUnreadNotificationCountResponse extends Message { - /** - * @generated from field: repeated string uris = 1; - */ - uris: string[] = []; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFeedGeneratorsRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetFeedGeneratorsRequest { - return new GetFeedGeneratorsRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetFeedGeneratorsRequest { - return new GetFeedGeneratorsRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetFeedGeneratorsRequest { - return new GetFeedGeneratorsRequest().fromJsonString(jsonString, options); - } - - static equals(a: GetFeedGeneratorsRequest | PlainMessage | undefined, b: GetFeedGeneratorsRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFeedGeneratorsRequest, a, b); - } -} - -/** - * @generated from message bsky.GetFeedGeneratorsResponse - */ -export class GetFeedGeneratorsResponse extends Message { - /** - * @generated from field: repeated bytes records = 1; - */ - records: Uint8Array[] = []; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetFeedGeneratorsResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "records", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetFeedGeneratorsResponse { - return new GetFeedGeneratorsResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetFeedGeneratorsResponse { - return new GetFeedGeneratorsResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetFeedGeneratorsResponse { - return new GetFeedGeneratorsResponse().fromJsonString(jsonString, options); - } - - static equals(a: GetFeedGeneratorsResponse | PlainMessage | undefined, b: GetFeedGeneratorsResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetFeedGeneratorsResponse, a, b); - } -} - /** * - Return uris of feed generator records created by user A * - `getActorFeeds` @@ -3324,9 +4622,9 @@ export class GetAuthorFeedRequest extends Message { cursor = ""; /** - * @generated from field: bool replies_only = 4; + * @generated from field: bool no_replies = 4; */ - repliesOnly = false; + noReplies = false; /** * @generated from field: bool media_only = 5; @@ -3344,7 +4642,7 @@ export class GetAuthorFeedRequest extends Message { { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 4, name: "replies_only", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, + { no: 4, name: "no_replies", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, { no: 5, name: "media_only", kind: "scalar", T: 8 /* ScalarType.BOOL */ }, ]); @@ -3370,9 +4668,9 @@ export class GetAuthorFeedRequest extends Message { */ export class GetAuthorFeedResponse extends Message { /** - * @generated from field: repeated string uris = 1; + * @generated from field: repeated bsky.FeedItem items = 1; */ - uris: string[] = []; + items: FeedItem[] = []; /** * @generated from field: string cursor = 2; @@ -3387,7 +4685,7 @@ export class GetAuthorFeedResponse extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "bsky.GetAuthorFeedResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "items", kind: "message", T: FeedItem, repeated: true }, { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); @@ -3465,9 +4763,9 @@ export class GetTimelineRequest extends Message { */ export class GetTimelineResponse extends Message { /** - * @generated from field: repeated string uris = 1; + * @generated from field: repeated bsky.FeedItem items = 1; */ - uris: string[] = []; + items: FeedItem[] = []; /** * @generated from field: string cursor = 2; @@ -3482,7 +4780,7 @@ export class GetTimelineResponse extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "bsky.GetTimelineResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "items", kind: "message", T: FeedItem, repeated: true }, { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); @@ -3503,6 +4801,49 @@ export class GetTimelineResponse extends Message { } } +/** + * @generated from message bsky.FeedItem + */ +export class FeedItem extends Message { + /** + * @generated from field: string uri = 1; + */ + uri = ""; + + /** + * @generated from field: string repost = 2; + */ + repost = ""; + + constructor(data?: PartialMessage) { + super(); + proto3.util.initPartial(data, this); + } + + static readonly runtime: typeof proto3 = proto3; + static readonly typeName = "bsky.FeedItem"; + static readonly fields: FieldList = proto3.util.newFieldList(() => [ + { no: 1, name: "uri", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "repost", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + ]); + + static fromBinary(bytes: Uint8Array, options?: Partial): FeedItem { + return new FeedItem().fromBinary(bytes, options); + } + + static fromJson(jsonValue: JsonValue, options?: Partial): FeedItem { + return new FeedItem().fromJson(jsonValue, options); + } + + static fromJsonString(jsonString: string, options?: Partial): FeedItem { + return new FeedItem().fromJsonString(jsonString, options); + } + + static equals(a: FeedItem | PlainMessage | undefined, b: FeedItem | PlainMessage | undefined): boolean { + return proto3.util.equals(FeedItem, a, b); + } +} + /** * - Return recent post uris from users in list A * - `getListFeed` @@ -3687,82 +5028,6 @@ export class GetThreadResponse extends Message { } } -/** - * Return threadgate records with uris A, B, C… - * - * @generated from message bsky.GetThreadgatesRequest - */ -export class GetThreadgatesRequest extends Message { - /** - * @generated from field: repeated string uris = 1; - */ - uris: string[] = []; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetThreadgatesRequest"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetThreadgatesRequest { - return new GetThreadgatesRequest().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetThreadgatesRequest { - return new GetThreadgatesRequest().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetThreadgatesRequest { - return new GetThreadgatesRequest().fromJsonString(jsonString, options); - } - - static equals(a: GetThreadgatesRequest | PlainMessage | undefined, b: GetThreadgatesRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetThreadgatesRequest, a, b); - } -} - -/** - * @generated from message bsky.GetThreadgatesResponse - */ -export class GetThreadgatesResponse extends Message { - /** - * @generated from field: repeated bytes records = 1; - */ - records: Uint8Array[] = []; - - constructor(data?: PartialMessage) { - super(); - proto3.util.initPartial(data, this); - } - - static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetThreadgatesResponse"; - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "records", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, - ]); - - static fromBinary(bytes: Uint8Array, options?: Partial): GetThreadgatesResponse { - return new GetThreadgatesResponse().fromBinary(bytes, options); - } - - static fromJson(jsonValue: JsonValue, options?: Partial): GetThreadgatesResponse { - return new GetThreadgatesResponse().fromJson(jsonValue, options); - } - - static fromJsonString(jsonString: string, options?: Partial): GetThreadgatesResponse { - return new GetThreadgatesResponse().fromJsonString(jsonString, options); - } - - static equals(a: GetThreadgatesResponse | PlainMessage | undefined, b: GetThreadgatesResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetThreadgatesResponse, a, b); - } -} - /** * - Return DIDs of actors matching term, paginated * - `searchActors` skeleton @@ -3955,60 +5220,66 @@ export class SearchPostsResponse extends Message { /** * - Return DIDs of suggested follows for a user, excluding anyone they already follow - * - `getSuggestions` + * - `getSuggestions`, `getSuggestedFollowsByActor` * - * @generated from message bsky.GetSuggestionsRequest + * @generated from message bsky.GetFollowSuggestionsRequest */ -export class GetSuggestionsRequest extends Message { +export class GetFollowSuggestionsRequest extends Message { /** * @generated from field: string actor_did = 1; */ actorDid = ""; /** - * @generated from field: int32 limit = 2; + * @generated from field: string relative_to_did = 2; + */ + relativeToDid = ""; + + /** + * @generated from field: int32 limit = 3; */ limit = 0; /** - * @generated from field: string cursor = 3; + * @generated from field: string cursor = 4; */ cursor = ""; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetSuggestionsRequest"; + static readonly typeName = "bsky.GetFollowSuggestionsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "actor_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 2, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, - { no: 3, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 2, name: "relative_to_did", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "limit", kind: "scalar", T: 5 /* ScalarType.INT32 */ }, + { no: 4, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetSuggestionsRequest { - return new GetSuggestionsRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetSuggestionsRequest { - return new GetSuggestionsRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetSuggestionsRequest { - return new GetSuggestionsRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFollowSuggestionsRequest { + return new GetFollowSuggestionsRequest().fromJsonString(jsonString, options); } - static equals(a: GetSuggestionsRequest | PlainMessage | undefined, b: GetSuggestionsRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetSuggestionsRequest, a, b); + static equals(a: GetFollowSuggestionsRequest | PlainMessage | undefined, b: GetFollowSuggestionsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowSuggestionsRequest, a, b); } } /** - * @generated from message bsky.GetSuggestionsResponse + * @generated from message bsky.GetFollowSuggestionsResponse */ -export class GetSuggestionsResponse extends Message { +export class GetFollowSuggestionsResponse extends Message { /** * @generated from field: repeated string dids = 1; */ @@ -4019,186 +5290,186 @@ export class GetSuggestionsResponse extends Message { */ cursor = ""; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetSuggestionsResponse"; + static readonly typeName = "bsky.GetFollowSuggestionsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 2, name: "cursor", kind: "scalar", T: 9 /* ScalarType.STRING */ }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetSuggestionsResponse { - return new GetSuggestionsResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetSuggestionsResponse { - return new GetSuggestionsResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetSuggestionsResponse { - return new GetSuggestionsResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetFollowSuggestionsResponse { + return new GetFollowSuggestionsResponse().fromJsonString(jsonString, options); } - static equals(a: GetSuggestionsResponse | PlainMessage | undefined, b: GetSuggestionsResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetSuggestionsResponse, a, b); + static equals(a: GetFollowSuggestionsResponse | PlainMessage | undefined, b: GetFollowSuggestionsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetFollowSuggestionsResponse, a, b); } } /** - * - Return post records with uris A, B, C… + * - Return post reply count with uris A, B, C… * - All feed hydration * - * @generated from message bsky.GetPostsRequest + * @generated from message bsky.GetPostReplyCountsRequest */ -export class GetPostsRequest extends Message { +export class GetPostReplyCountsRequest extends Message { /** * @generated from field: repeated string uris = 1; */ uris: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetPostsRequest"; + static readonly typeName = "bsky.GetPostReplyCountsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetPostsRequest { - return new GetPostsRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetPostsRequest { - return new GetPostsRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetPostsRequest { - return new GetPostsRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetPostReplyCountsRequest { + return new GetPostReplyCountsRequest().fromJsonString(jsonString, options); } - static equals(a: GetPostsRequest | PlainMessage | undefined, b: GetPostsRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetPostsRequest, a, b); + static equals(a: GetPostReplyCountsRequest | PlainMessage | undefined, b: GetPostReplyCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostReplyCountsRequest, a, b); } } /** - * @generated from message bsky.GetPostsResponse + * @generated from message bsky.GetPostReplyCountsResponse */ -export class GetPostsResponse extends Message { +export class GetPostReplyCountsResponse extends Message { /** - * @generated from field: repeated bytes records = 1; + * @generated from field: repeated int32 counts = 1; */ - records: Uint8Array[] = []; + counts: number[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetPostsResponse"; + static readonly typeName = "bsky.GetPostReplyCountsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "records", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetPostsResponse { - return new GetPostsResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetPostsResponse { - return new GetPostsResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetPostsResponse { - return new GetPostsResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetPostReplyCountsResponse { + return new GetPostReplyCountsResponse().fromJsonString(jsonString, options); } - static equals(a: GetPostsResponse | PlainMessage | undefined, b: GetPostsResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetPostsResponse, a, b); + static equals(a: GetPostReplyCountsResponse | PlainMessage | undefined, b: GetPostReplyCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostReplyCountsResponse, a, b); } } /** - * - Return post reply count with uris A, B, C… - * - All feed hydration + * - Return post count for users A, B, C… + * - Profile hydration * - * @generated from message bsky.GetPostReplyCountRequest + * @generated from message bsky.GetPostCountsRequest */ -export class GetPostReplyCountRequest extends Message { +export class GetPostCountsRequest extends Message { /** - * @generated from field: repeated string uris = 1; + * @generated from field: repeated string dids = 1; */ - uris: string[] = []; + dids: string[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetPostReplyCountRequest"; + static readonly typeName = "bsky.GetPostCountsRequest"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "uris", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 1, name: "dids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetPostReplyCountRequest { - return new GetPostReplyCountRequest().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostCountsRequest { + return new GetPostCountsRequest().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetPostReplyCountRequest { - return new GetPostReplyCountRequest().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostCountsRequest { + return new GetPostCountsRequest().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetPostReplyCountRequest { - return new GetPostReplyCountRequest().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetPostCountsRequest { + return new GetPostCountsRequest().fromJsonString(jsonString, options); } - static equals(a: GetPostReplyCountRequest | PlainMessage | undefined, b: GetPostReplyCountRequest | PlainMessage | undefined): boolean { - return proto3.util.equals(GetPostReplyCountRequest, a, b); + static equals(a: GetPostCountsRequest | PlainMessage | undefined, b: GetPostCountsRequest | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostCountsRequest, a, b); } } /** - * @generated from message bsky.GetPostReplyCountResponse + * @generated from message bsky.GetPostCountsResponse */ -export class GetPostReplyCountResponse extends Message { +export class GetPostCountsResponse extends Message { /** * @generated from field: repeated int32 counts = 1; */ counts: number[] = []; - constructor(data?: PartialMessage) { + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); } static readonly runtime: typeof proto3 = proto3; - static readonly typeName = "bsky.GetPostReplyCountResponse"; + static readonly typeName = "bsky.GetPostCountsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "counts", kind: "scalar", T: 5 /* ScalarType.INT32 */, repeated: true }, ]); - static fromBinary(bytes: Uint8Array, options?: Partial): GetPostReplyCountResponse { - return new GetPostReplyCountResponse().fromBinary(bytes, options); + static fromBinary(bytes: Uint8Array, options?: Partial): GetPostCountsResponse { + return new GetPostCountsResponse().fromBinary(bytes, options); } - static fromJson(jsonValue: JsonValue, options?: Partial): GetPostReplyCountResponse { - return new GetPostReplyCountResponse().fromJson(jsonValue, options); + static fromJson(jsonValue: JsonValue, options?: Partial): GetPostCountsResponse { + return new GetPostCountsResponse().fromJson(jsonValue, options); } - static fromJsonString(jsonString: string, options?: Partial): GetPostReplyCountResponse { - return new GetPostReplyCountResponse().fromJsonString(jsonString, options); + static fromJsonString(jsonString: string, options?: Partial): GetPostCountsResponse { + return new GetPostCountsResponse().fromJsonString(jsonString, options); } - static equals(a: GetPostReplyCountResponse | PlainMessage | undefined, b: GetPostReplyCountResponse | PlainMessage | undefined): boolean { - return proto3.util.equals(GetPostReplyCountResponse, a, b); + static equals(a: GetPostCountsResponse | PlainMessage | undefined, b: GetPostCountsResponse | PlainMessage | undefined): boolean { + return proto3.util.equals(GetPostCountsResponse, a, b); } } @@ -4253,9 +5524,9 @@ export class GetLabelsRequest extends Message { */ export class GetLabelsResponse extends Message { /** - * @generated from field: repeated bytes records = 1; + * @generated from field: repeated bytes labels = 1; */ - records: Uint8Array[] = []; + labels: Uint8Array[] = []; constructor(data?: PartialMessage) { super(); @@ -4265,7 +5536,7 @@ export class GetLabelsResponse extends Message { static readonly runtime: typeof proto3 = proto3; static readonly typeName = "bsky.GetLabelsResponse"; static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: "records", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, + { no: 1, name: "labels", kind: "scalar", T: 12 /* ScalarType.BYTES */, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): GetLabelsResponse { diff --git a/packages/bsky/src/data-plane/index.ts b/packages/bsky/src/data-plane/index.ts new file mode 100644 index 00000000000..6047188a655 --- /dev/null +++ b/packages/bsky/src/data-plane/index.ts @@ -0,0 +1,2 @@ +export * from './server' +export * from './client' diff --git a/packages/bsky/src/data-plane/server/index.ts b/packages/bsky/src/data-plane/server/index.ts index 6ceec06fd72..554d5bd2f7d 100644 --- a/packages/bsky/src/data-plane/server/index.ts +++ b/packages/bsky/src/data-plane/server/index.ts @@ -17,7 +17,7 @@ export class DataPlaneServer { return new DataPlaneServer(server) } - async stop() { + async destroy() { return new Promise((resolve, reject) => { this.server.close((err) => { if (err) { diff --git a/packages/bsky/src/data-plane/server/routes/blocks.ts b/packages/bsky/src/data-plane/server/routes/blocks.ts index 66a0fdaaec5..333163945dc 100644 --- a/packages/bsky/src/data-plane/server/routes/blocks.ts +++ b/packages/bsky/src/data-plane/server/routes/blocks.ts @@ -87,7 +87,7 @@ export default (db: Database): Partial> => ({ .limit(1) .executeTakeFirst() return { - subscribed: !!res, + listblockUri: res?.uri, } }, diff --git a/packages/bsky/src/data-plane/server/routes/feed-gens.ts b/packages/bsky/src/data-plane/server/routes/feed-gens.ts index 92df9a1a082..2dc46a98386 100644 --- a/packages/bsky/src/data-plane/server/routes/feed-gens.ts +++ b/packages/bsky/src/data-plane/server/routes/feed-gens.ts @@ -1,29 +1,9 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' -import * as ui8 from 'uint8arrays' import { Database } from '../../../db' -import { keyBy } from '@atproto/common' import { TimeCidKeyset, paginate } from '../../../db/pagination' export default (db: Database): Partial> => ({ - async getFeedGenerators(req) { - if (req.uris.length === 0) { - return { records: [] } - } - const res = await db.db - .selectFrom('record') - .selectAll() - .where('uri', 'in', req.uris) - .execute() - const byUri = keyBy(res, 'uri') - const records = req.uris.map((uri) => { - const row = byUri[uri] - const json = row ? row.json : JSON.stringify(null) - return ui8.fromString(json, 'utf8') - }) - return { records } - }, - async getActorFeeds(req) { const { actorDid, limit, cursor } = req diff --git a/packages/bsky/src/data-plane/server/routes/feeds.ts b/packages/bsky/src/data-plane/server/routes/feeds.ts index f0620b8d631..79a01a48530 100644 --- a/packages/bsky/src/data-plane/server/routes/feeds.ts +++ b/packages/bsky/src/data-plane/server/routes/feeds.ts @@ -5,12 +5,13 @@ import { TimeCidKeyset, paginate } from '../../../db/pagination' export default (db: Database): Partial> => ({ async getAuthorFeed(req) { - const { actorDid, limit, cursor, repliesOnly, mediaOnly } = req + const { actorDid, limit, cursor, noReplies, mediaOnly } = req const { ref } = db.db.dynamic // defaults to posts, reposts, and replies let builder = db.db .selectFrom('feed_item') + .innerJoin('post', 'post.uri', 'feed_item.postUri') .selectAll('feed_item') .where('originatorDid', '=', actorDid) @@ -25,8 +26,10 @@ export default (db: Database): Partial> => ({ .select('post_embed_image.postUri') .whereRef('post_embed_image.postUri', '=', 'feed_item.postUri'), ) - } else if (repliesOnly) { - // @TODO + } else if (noReplies) { + builder = builder.where((qb) => + qb.where('post.replyParent', 'is', null).orWhere('type', '=', 'repost'), + ) } const keyset = new TimeCidKeyset( @@ -43,7 +46,7 @@ export default (db: Database): Partial> => ({ const feedItems = await builder.execute() return { - uris: feedItems.map((row) => row.uri), + items: feedItems.map(feedItemFromRow), cursor: keyset.packFromResult(feedItems), } }, @@ -96,7 +99,7 @@ export default (db: Database): Partial> => ({ .slice(0, limit) return { - uris: feedItems.map((item) => item.uri), + items: feedItems.map(feedItemFromRow), cursor: keyset.packFromResult(feedItems), } }, @@ -107,7 +110,7 @@ export default (db: Database): Partial> => ({ let builder = db.db .selectFrom('post') - .selectAll() + .selectAll('post') .innerJoin('list_item', 'list_item.subjectDid', 'post.creator') .where('list_item.listUri', '=', listUri) @@ -121,8 +124,15 @@ export default (db: Database): Partial> => ({ const feedItems = await builder.execute() return { - uris: feedItems.map((item) => item.uri), + uris: feedItems.map((item) => item.uri), // @TODO consider switching to FeedItemInfo[] cursor: keyset.packFromResult(feedItems), } }, }) + +const feedItemFromRow = (row: { postUri: string; uri: string }) => { + return { + uri: row.postUri, + repost: row.uri === row.postUri ? undefined : row.uri, + } +} diff --git a/packages/bsky/src/data-plane/server/routes/follows.ts b/packages/bsky/src/data-plane/server/routes/follows.ts index 3d3fc94a1f9..ecab294f3c3 100644 --- a/packages/bsky/src/data-plane/server/routes/follows.ts +++ b/packages/bsky/src/data-plane/server/routes/follows.ts @@ -33,6 +33,7 @@ export default (db: Database): Partial> => ({ .select([ 'follow.uri as uri', 'follow.cid as cid', + 'follow.subjectDid as subjectDid', 'follow.sortAt as sortAt', ]) @@ -41,11 +42,12 @@ export default (db: Database): Partial> => ({ limit, cursor, keyset, + tryIndex: true, }) const followers = await followersReq.execute() return { - uris: followers.map((f) => f.uri), + followers: followers.map((f) => ({ uri: f.uri, did: f.subjectDid })), cursor: keyset.packFromResult(followers), } }, @@ -61,6 +63,7 @@ export default (db: Database): Partial> => ({ .select([ 'follow.uri as uri', 'follow.cid as cid', + 'follow.subjectDid as subjectDid', 'follow.sortAt as sortAt', ]) @@ -69,33 +72,40 @@ export default (db: Database): Partial> => ({ limit, cursor, keyset, + tryIndex: true, }) const follows = await followsReq.execute() return { - uris: follows.map((f) => f.uri), + follows: follows.map((f) => ({ uri: f.uri, did: f.subjectDid })), cursor: keyset.packFromResult(follows), } }, - async getFollowersCount(req) { + async getFollowerCounts(req) { + if (req.dids.length === 0) { + return { counts: [] } + } const res = await db.db .selectFrom('profile_agg') - .select('followersCount') - .where('did', '=', req.actorDid) - .executeTakeFirst() - return { - count: res?.followersCount, - } + .selectAll() + .where('did', 'in', req.dids) + .execute() + const byDid = keyBy(res, 'did') + const counts = req.dids.map((did) => byDid[did]?.followersCount ?? 0) + return { counts } }, - async getFollowsCount(req) { + async getFollowCounts(req) { + if (req.dids.length === 0) { + return { counts: [] } + } const res = await db.db .selectFrom('profile_agg') - .select('followsCount') - .where('did', '=', req.actorDid) - .executeTakeFirst() - return { - count: res?.followsCount, - } + .selectAll() + .where('did', 'in', req.dids) + .execute() + const byDid = keyBy(res, 'did') + const counts = req.dids.map((did) => byDid[did]?.followsCount ?? 0) + return { counts } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/index.ts b/packages/bsky/src/data-plane/server/routes/index.ts index bc0b439fcf4..4eb4203a52e 100644 --- a/packages/bsky/src/data-plane/server/routes/index.ts +++ b/packages/bsky/src/data-plane/server/routes/index.ts @@ -13,6 +13,8 @@ import mutes from './mutes' import notifs from './notifs' import posts from './posts' import profile from './profile' +import records from './records' +import relationships from './relationships' import reposts from './reposts' import search from './search' import suggestions from './suggestions' @@ -34,6 +36,8 @@ export default (db: Database) => (router: ConnectRouter) => ...notifs(db), ...posts(db), ...profile(db), + ...records(db), + ...relationships(db), ...reposts(db), ...search(db), ...suggestions(db), diff --git a/packages/bsky/src/data-plane/server/routes/labels.ts b/packages/bsky/src/data-plane/server/routes/labels.ts index d7c94f74b0d..917c36a7ee2 100644 --- a/packages/bsky/src/data-plane/server/routes/labels.ts +++ b/packages/bsky/src/data-plane/server/routes/labels.ts @@ -5,24 +5,24 @@ import * as ui8 from 'uint8arrays' export default (db: Database): Partial> => ({ async getLabels(req) { - const { subjects, issuers } = req - if (subjects.length === 0 || issuers.length === 0) { + // @TODO add in issues param + const { subjects } = req + if (subjects.length === 0) { return { records: [] } } - const labels = await db.db + const res = await db.db .selectFrom('label') - .where('src', 'in', issuers) .where('uri', 'in', subjects) .selectAll() .execute() - const records = labels.map((l) => { + const labels = res.map((l) => { const formatted = { ...l, cid: l.cid === '' ? undefined : l.cid, } return ui8.fromString(JSON.stringify(formatted), 'utf8') }) - return { records } + return { labels } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/likes.ts b/packages/bsky/src/data-plane/server/routes/likes.ts index ef448369b6b..9765e05c401 100644 --- a/packages/bsky/src/data-plane/server/routes/likes.ts +++ b/packages/bsky/src/data-plane/server/routes/likes.ts @@ -2,15 +2,21 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { Database } from '../../../db' import { TimeCidKeyset, paginate } from '../../../db/pagination' +import { keyBy } from '@atproto/common' export default (db: Database): Partial> => ({ async getLikesBySubject(req) { - const { subjectUri, cursor, limit } = req + const { subject, cursor, limit } = req const { ref } = db.db.dynamic + if (!subject?.uri) { + return { uris: [] } + } + + // @NOTE ignoring subject.cid let builder = db.db .selectFrom('like') - .where('like.subject', '=', subjectUri) + .where('like.subject', '=', subject?.uri) .selectAll('like') const keyset = new TimeCidKeyset(ref('like.sortAt'), ref('like.cid')) @@ -28,15 +34,26 @@ export default (db: Database): Partial> => ({ } }, - async getLikeByActorAndSubject(req) { - const { actorDid, subjectUri } = req + async getLikesByActorAndSubjects(req) { + const { actorDid, refs } = req + if (refs.length === 0) { + return { uris: [] } + } + // @NOTE ignoring ref.cid const res = await db.db .selectFrom('like') .where('creator', '=', actorDid) - .where('subject', '=', subjectUri) - .select('uri') - .executeTakeFirst() - return { uri: res?.uri } + .where( + 'subject', + 'in', + refs.map(({ uri }) => uri), + ) + .selectAll() + .execute() + const bySubject = keyBy(res, 'subject') + // @TODO handling undefineds properly, or do we need to turn them into empty strings? + const uris = refs.map(({ uri }) => bySubject[uri]?.uri) + return { uris } }, async getActorLikes(req) { @@ -59,19 +76,25 @@ export default (db: Database): Partial> => ({ const likes = await builder.execute() return { - uris: likes.map((l) => l.uri), + likes: likes.map((l) => ({ + uri: l.uri, + subject: l.subject, + })), cursor: keyset.packFromResult(likes), } }, - async getLikesCount(req) { + async getLikeCounts(req) { + if (req.uris.length === 0) { + return { counts: [] } + } const res = await db.db .selectFrom('post_agg') - .where('uri', '=', req.subjectUri) - .select('likeCount') - .executeTakeFirst() - return { - count: res?.likeCount, - } + .where('uri', 'in', req.uris) + .selectAll() + .execute() + const byUri = keyBy(res, 'uri') + const counts = req.uris.map((uri) => byUri[uri]?.likeCount ?? 0) + return { counts } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/lists.ts b/packages/bsky/src/data-plane/server/routes/lists.ts index 1cf3f6a2d72..401071c313e 100644 --- a/packages/bsky/src/data-plane/server/routes/lists.ts +++ b/packages/bsky/src/data-plane/server/routes/lists.ts @@ -1,12 +1,32 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' -import * as ui8 from 'uint8arrays' import { Database } from '../../../db' import { countAll } from '../../../db/util' import { keyBy } from '@atproto/common' import { TimeCidKeyset, paginate } from '../../../db/pagination' export default (db: Database): Partial> => ({ + async getActorLists(req) { + const { actorDid, cursor, limit } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('list') + .where('creator', '=', actorDid) + .selectAll() + const keyset = new TimeCidKeyset(ref('list.sortAt'), ref('list.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + const lists = await builder.execute() + return { + listUris: lists.map((item) => item.uri), + cursor: keyset.packFromResult(lists), + } + }, + async getListMembers(req) { const { listUri, cursor, limit } = req const { ref } = db.db.dynamic @@ -24,11 +44,15 @@ export default (db: Database): Partial> => ({ limit, cursor, keyset, + tryIndex: true, }) const listItems = await builder.execute() return { - dids: listItems.map((item) => item.subjectDid), + listitems: listItems.map((item) => ({ + uri: item.uri, + did: item.subjectDid, + })), cursor: keyset.packFromResult(listItems), } }, @@ -51,18 +75,6 @@ export default (db: Database): Partial> => ({ } }, - async getList(req) { - const res = await db.db - .selectFrom('record') - .where('uri', '=', req.listUri) - .select('json') - .executeTakeFirst() - const record = res ? ui8.fromString(res.json, 'utf8') : undefined - return { - record, - } - }, - async getListCount(req) { const res = await db.db .selectFrom('list_item') diff --git a/packages/bsky/src/data-plane/server/routes/notifs.ts b/packages/bsky/src/data-plane/server/routes/notifs.ts index aa77357982f..2609f98e436 100644 --- a/packages/bsky/src/data-plane/server/routes/notifs.ts +++ b/packages/bsky/src/data-plane/server/routes/notifs.ts @@ -1,9 +1,10 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { Database } from '../../../db' -import { countAll, excluded } from '../../../db/util' +import { countAll, excluded, notSoftDeletedClause } from '../../../db/util' import { sql } from 'kysely' import { TimeCidKeyset, paginate } from '../../../db/pagination' +import { Timestamp } from '@bufbuild/protobuf' export default (db: Database): Partial> => ({ async getNotifications(req) { @@ -46,9 +47,8 @@ export default (db: Database): Partial> => ({ const notifications = notifsRes.map((notif) => ({ uri: notif.uri, reason: notif.reason, - timestamp: { - nanos: new Date(notif.sortAt).getTime() * 1000, - }, + reasonSubject: notif.reasonSubject ?? undefined, + timestamp: Timestamp.fromDate(new Date(notif.sortAt)), })) return { notifications, @@ -65,11 +65,8 @@ export default (db: Database): Partial> => ({ if (!res) { return {} } - const nanos = new Date(res.lastSeenNotifs).getTime() * 1000 return { - timestamp: { - nanos, - }, + timestamp: Timestamp.fromDate(new Date(res.lastSeenNotifs)), } }, @@ -82,6 +79,8 @@ export default (db: Database): Partial> => ({ .innerJoin('actor', 'actor.did', 'notification.did') .leftJoin('actor_state', 'actor_state.did', 'actor.did') .innerJoin('record', 'record.uri', 'notification.recordUri') + .where(notSoftDeletedClause(ref('record'))) + .where(notSoftDeletedClause(ref('actor'))) // Ensure to hit notification_did_sortat_idx, handling case where lastSeenNotifs is null. .where('notification.did', '=', actorDid) .where( @@ -101,9 +100,7 @@ export default (db: Database): Partial> => ({ if (!timestamp) { return } - const lastSeenNotifs = new Date( - Math.floor(timestamp.nanos / 1000), - ).toISOString() + const lastSeenNotifs = timestamp.toDate().toISOString() await db.db .insertInto('actor_state') .values({ did: actorDid, lastSeenNotifs }) diff --git a/packages/bsky/src/data-plane/server/routes/posts.ts b/packages/bsky/src/data-plane/server/routes/posts.ts index 97c25fc7754..f9621cbce5e 100644 --- a/packages/bsky/src/data-plane/server/routes/posts.ts +++ b/packages/bsky/src/data-plane/server/routes/posts.ts @@ -1,38 +1,33 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { keyBy } from '@atproto/common' -import * as ui8 from 'uint8arrays' import { Database } from '../../../db' export default (db: Database): Partial> => ({ - async getPosts(req) { + async getPostReplyCounts(req) { if (req.uris.length === 0) { - return { records: [] } + return { counts: [] } } const res = await db.db - .selectFrom('record') - .selectAll() + .selectFrom('post_agg') + .select(['uri', 'replyCount']) .where('uri', 'in', req.uris) .execute() const byUri = keyBy(res, 'uri') - const records = req.uris.map((uri) => { - const row = byUri[uri] - const json = row ? row.json : JSON.stringify(null) - return ui8.fromString(json, 'utf8') - }) - return { records } + const counts = req.uris.map((uri) => byUri[uri]?.replyCount ?? 0) + return { counts } }, - async getPostReplyCount(req) { - if (req.uris.length === 0) { + async getPostCounts(req) { + if (req.dids.length === 0) { return { counts: [] } } const res = await db.db - .selectFrom('post_agg') - .select(['uri', 'replyCount']) - .where('uri', 'in', req.uris) + .selectFrom('profile_agg') + .selectAll() + .where('did', 'in', req.dids) .execute() - const byUri = keyBy(res, 'uri') - const counts = req.uris.map((uri) => byUri[uri]?.replyCount ?? 0) + const byDid = keyBy(res, 'did') + const counts = req.dids.map((did) => byDid[did]?.postsCount ?? 0) return { counts } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/profile.ts b/packages/bsky/src/data-plane/server/routes/profile.ts index bb0ab452885..aca3f22eaea 100644 --- a/packages/bsky/src/data-plane/server/routes/profile.ts +++ b/packages/bsky/src/data-plane/server/routes/profile.ts @@ -1,43 +1,42 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { keyBy } from '@atproto/common' -import * as ui8 from 'uint8arrays' import { Database } from '../../../db' +import { getRecords } from './records' export default (db: Database): Partial> => ({ - async getProfiles(req) { - const { dids } = req - if (dids.length === 0) { - return { records: [] } + async getActors(req) { + try { + const { dids } = req + if (dids.length === 0) { + return { actors: [] } + } + const profileUris = dids.map( + (did) => `at://${did}/app.bsky.actor.profile/self`, + ) + const [handlesRes, profiles] = await Promise.all([ + db.db + .selectFrom('actor') + .where('did', 'in', dids) + .selectAll() + .execute(), + getRecords(db)({ uris: profileUris }), + ]) + const byDid = keyBy(handlesRes, 'did') + const actors = dids.map((did, i) => { + const row = byDid[did] + return { + exists: !!row, + handle: row?.handle ?? undefined, + profile: profiles.records[i], + takenDown: !!row?.takedownId, + } + }) + return { actors } + } catch (err) { + console.log(err) + throw err } - const uris = dids.map((did) => `at://${did}/app.bsky.actor.profile/self`) - const res = await db.db - .selectFrom('record') - .selectAll() - .where('uri', 'in', uris) - .execute() - const byUri = keyBy(res, 'uri') - const records = uris.map((uri) => { - const row = byUri[uri] - const json = row ? row.json : JSON.stringify(null) - return ui8.fromString(json, 'utf8') - }) - return { records } - }, - - async getHandles(req) { - const { dids } = req - if (dids.length === 0) { - return { handles: [] } - } - const res = await db.db - .selectFrom('actor') - .where('did', 'in', dids) - .selectAll() - .execute() - const byDid = keyBy(res, 'did') - const handles = dids.map((did) => byDid[did]?.handle ?? '') - return { handles } }, async getDidsByHandles(req) { diff --git a/packages/bsky/src/data-plane/server/routes/records.ts b/packages/bsky/src/data-plane/server/routes/records.ts new file mode 100644 index 00000000000..9c6c9e3a730 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/records.ts @@ -0,0 +1,79 @@ +import { keyBy } from '@atproto/common' +import { AtUri } from '@atproto/syntax' +import { Timestamp } from '@bufbuild/protobuf' +import { ServiceImpl } from '@connectrpc/connect' +import * as ui8 from 'uint8arrays' +import { ids } from '../../../lexicon/lexicons' +import { Service } from '../../gen/bsky_connect' +import { PostRecordMeta, Record } from '../../gen/bsky_pb' +import { Database } from '../../../db' + +export default (db: Database): Partial> => ({ + getBlockRecords: getRecords(db, ids.AppBskyGraphBlock), + getFeedGeneratorRecords: getRecords(db, ids.AppBskyFeedGenerator), + getFollowRecords: getRecords(db, ids.AppBskyGraphFollow), + getLikeRecords: getRecords(db, ids.AppBskyFeedLike), + getListBlockRecords: getRecords(db, ids.AppBskyGraphListblock), + getListItemRecords: getRecords(db, ids.AppBskyGraphListitem), + getListRecords: getRecords(db, ids.AppBskyGraphList), + getPostRecords: getPostRecords(db), + getProfileRecords: getRecords(db, ids.AppBskyActorProfile), + getRepostRecords: getRecords(db, ids.AppBskyFeedRepost), + getThreadGateRecords: getRecords(db, ids.AppBskyFeedThreadgate), +}) + +export const getRecords = + (db: Database, collection?: string) => + async (req: { uris: string[] }): Promise<{ records: Record[] }> => { + const validUris = collection + ? req.uris.filter((uri) => new AtUri(uri).collection === collection) + : req.uris + const res = validUris.length + ? await db.db + .selectFrom('record') + .selectAll() + .where('uri', 'in', validUris) + .execute() + : [] + const byUri = keyBy(res, 'uri') + const records: Record[] = req.uris.map((uri) => { + const row = byUri[uri] + const json = row ? row.json : JSON.stringify(null) + const indexedAt = row?.indexedAt + ? Timestamp.fromDate(new Date(row?.indexedAt)) + : undefined + const recordBytes = ui8.fromString(json, 'utf8') + return new Record({ + record: recordBytes, + cid: row?.cid, + indexedAt, + takenDown: !!row?.takedownId, + }) + }) + return { records } + } + +export const getPostRecords = (db: Database) => { + const getBaseRecords = getRecords(db, ids.AppBskyFeedPost) + return async (req: { + uris: string[] + }): Promise<{ records: Record[]; meta: PostRecordMeta[] }> => { + const [{ records }, details] = await Promise.all([ + getBaseRecords(req), + req.uris.length + ? await db.db + .selectFrom('post') + .where('uri', 'in', req.uris) + .select(['uri', 'violatesThreadGate']) + .execute() + : [], + ]) + const byKey = keyBy(details, 'uri') + const meta = req.uris.map((uri) => { + return new PostRecordMeta({ + violatesThreadGate: !!byKey[uri]?.violatesThreadGate, + }) + }) + return { records, meta } + } +} diff --git a/packages/bsky/src/data-plane/server/routes/relationships.ts b/packages/bsky/src/data-plane/server/routes/relationships.ts new file mode 100644 index 00000000000..c2e6ce33772 --- /dev/null +++ b/packages/bsky/src/data-plane/server/routes/relationships.ts @@ -0,0 +1,148 @@ +import { ServiceImpl } from '@connectrpc/connect' +import { Service } from '../../gen/bsky_connect' +import { keyBy } from '@atproto/common' +import { Database } from '../../../db' +import { sql } from 'kysely' +import { valuesList } from '../../../db/util' + +export default (db: Database): Partial> => ({ + async getRelationships(req) { + const { actorDid, targetDids } = req + if (targetDids.length === 0) { + return { relationships: [] } + } + const { ref } = db.db.dynamic + const res = await db.db + .selectFrom('actor') + .where('did', 'in', targetDids) + .select([ + 'actor.did', + db.db + .selectFrom('mute') + .where('mute.mutedByDid', '=', actorDid) + .whereRef('mute.subjectDid', '=', ref('actor.did')) + .select(sql`${true}`.as('val')) + .as('muted'), + db.db + .selectFrom('list_item') + .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') + .where('list_mute.mutedByDid', '=', actorDid) + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .as('mutedByList'), + db.db + .selectFrom('actor_block') + .where('actor_block.creator', '=', actorDid) + .whereRef('actor_block.subjectDid', '=', ref('actor.did')) + .select('uri') + .as('blocking'), + db.db + .selectFrom('actor_block') + .where('actor_block.subjectDid', '=', actorDid) + .whereRef('actor_block.creator', '=', ref('actor.did')) + .select('uri') + .as('blockedBy'), + db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .where('list_block.creator', '=', actorDid) + .whereRef('list_item.subjectDid', '=', ref('actor.did')) + .select('list_item.listUri') + .as('blockingByList'), + db.db + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .where('list_item.subjectDid', '=', actorDid) + .whereRef('list_block.creator', '=', ref('actor.did')) + .select('list_item.listUri') + .as('blockedByList'), + db.db + .selectFrom('follow') + .where('follow.creator', '=', actorDid) + .whereRef('follow.subjectDid', '=', ref('actor.did')) + .select('uri') + .as('following'), + db.db + .selectFrom('follow') + .where('follow.subjectDid', '=', actorDid) + .whereRef('follow.creator', '=', ref('actor.did')) + .select('uri') + .as('followedBy'), + ]) + .execute() + const byDid = keyBy(res, 'did') + const relationships = targetDids.map((did) => { + const row = byDid[did] ?? {} + return { + muted: row.muted ?? false, + mutedByList: row.mutedByList ?? '', + blockedBy: row.blockedBy ?? '', + blocking: row.blocking ?? '', + blockedByList: row.blockedByList ?? '', + blockingByList: row.blockingByList ?? '', + following: row.following ?? '', + followedBy: row.followedBy ?? '', + } + }) + return { relationships } + }, + + async getBlockExistence(req) { + const { pairs } = req + if (pairs.length === 0) { + return { exists: [] } + } + const { ref } = db.db.dynamic + const sourceRef = ref('pair.source') + const targetRef = ref('pair.target') + const values = valuesList(pairs.map((p) => sql`${p.a}, ${p.b}`)) + const res = await db.db + .selectFrom(values.as(sql`pair (source, target)`)) + .select([ + sql`${sourceRef}`.as('source'), + sql`${targetRef}`.as('target'), + ]) + .whereExists((qb) => + qb + .selectFrom('actor_block') + .whereRef('actor_block.creator', '=', sourceRef) + .whereRef('actor_block.subjectDid', '=', targetRef) + .select('uri'), + ) + .orWhereExists((qb) => + qb + .selectFrom('actor_block') + .whereRef('actor_block.creator', '=', targetRef) + .whereRef('actor_block.subjectDid', '=', sourceRef) + .select('uri'), + ) + .orWhereExists((qb) => + qb + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', sourceRef) + .whereRef('list_item.subjectDid', '=', targetRef) + .select('list_item.listUri'), + ) + .orWhereExists((qb) => + qb + .selectFrom('list_item') + .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') + .whereRef('list_block.creator', '=', targetRef) + .whereRef('list_item.subjectDid', '=', sourceRef) + .select('list_item.listUri'), + ) + .execute() + const existMap = res.reduce((acc, cur) => { + const key = [cur.source, cur.target].sort().join(',') + return acc.set(key, true) + }, new Map()) + const exists = pairs.map((pair) => { + const key = [pair.a, pair.b].sort().join(',') + return existMap.get(key) === true + }) + return { + exists, + } + }, +}) diff --git a/packages/bsky/src/data-plane/server/routes/reposts.ts b/packages/bsky/src/data-plane/server/routes/reposts.ts index 0e995aa0b61..2d9f4da1a46 100644 --- a/packages/bsky/src/data-plane/server/routes/reposts.ts +++ b/packages/bsky/src/data-plane/server/routes/reposts.ts @@ -2,6 +2,7 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { Database } from '../../../db' import { TimeCidKeyset, paginate } from '../../../db/pagination' +import { keyBy } from '@atproto/common' export default (db: Database): Partial> => ({ async getRepostsBySubject(req) { @@ -28,15 +29,25 @@ export default (db: Database): Partial> => ({ } }, - async getRepostByActorAndSubject(req) { - const { actorDid, subjectUri } = req + async getRepostsByActorAndSubjects(req) { + const { actorDid, refs } = req + if (refs.length === 0) { + return { uris: [] } + } const res = await db.db .selectFrom('repost') .where('creator', '=', actorDid) - .where('subject', '=', subjectUri) - .select('uri') - .executeTakeFirst() - return { uri: res?.uri } + .where( + 'subject', + 'in', + refs.map(({ uri }) => uri), + ) + .selectAll() + .execute() + const bySubject = keyBy(res, 'subject') + // @TODO handling undefineds properly, or do we need to turn them into empty strings? + const uris = refs.map(({ uri }) => bySubject[uri]?.uri) + return { uris } }, async getActorReposts(req) { @@ -64,14 +75,17 @@ export default (db: Database): Partial> => ({ } }, - async getRepostsCount(req) { + async getRepostCounts(req) { + if (req.uris.length === 0) { + return { counts: [] } + } const res = await db.db .selectFrom('post_agg') - .where('uri', '=', req.subjectUri) - .select('repostCount') - .executeTakeFirst() - return { - count: res?.repostCount, - } + .where('uri', 'in', req.uris) + .selectAll() + .execute() + const byUri = keyBy(res, 'uri') + const counts = req.uris.map((uri) => byUri[uri]?.repostCount ?? 0) + return { counts } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/search.ts b/packages/bsky/src/data-plane/server/routes/search.ts index 7638d9399ca..101856553cd 100644 --- a/packages/bsky/src/data-plane/server/routes/search.ts +++ b/packages/bsky/src/data-plane/server/routes/search.ts @@ -1,12 +1,60 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { Database } from '../../../db' +import { + IndexedAtDidKeyset, + TimeCidKeyset, + paginate, +} from '../../../db/pagination' export default (db: Database): Partial> => ({ async searchActors(req) { - throw new Error('unimplemented') + const { term, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('actor') + .where('actor.handle', 'like', `%${term}%`) + .selectAll() + + const keyset = new IndexedAtDidKeyset( + ref('actor.indexedAt'), + ref('actor.did'), + ) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const res = await builder.execute() + + return { + dids: res.map((row) => row.did), + cursor: keyset.packFromResult(res), + } }, + async searchPosts(req) { - throw new Error('unimplemented') + const { term, limit, cursor } = req + const { ref } = db.db.dynamic + let builder = db.db + .selectFrom('post') + .where('post.text', 'like', `%${term}%`) + .selectAll() + + const keyset = new TimeCidKeyset(ref('post.sortAt'), ref('post.cid')) + builder = paginate(builder, { + limit, + cursor, + keyset, + tryIndex: true, + }) + + const res = await builder.execute() + return { + uris: res.map((row) => row.uri), + cursor: keyset.packFromResult(res), + } }, }) diff --git a/packages/bsky/src/data-plane/server/routes/suggestions.ts b/packages/bsky/src/data-plane/server/routes/suggestions.ts index 40ce8f13a5e..b1f80007b62 100644 --- a/packages/bsky/src/data-plane/server/routes/suggestions.ts +++ b/packages/bsky/src/data-plane/server/routes/suggestions.ts @@ -1,42 +1,147 @@ +import { sql } from 'kysely' import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' import { Database } from '../../../db' export default (db: Database): Partial> => ({ - async getSuggestions(req) { - const alreadyIncluded = parseCursor(req.cursor) - const suggestions = await db.db + async getFollowSuggestions(req) { + const { actorDid, relativeToDid, cursor, limit } = req + if (relativeToDid) { + return getFollowSuggestionsRelativeTo(db, { + actorDid, + relativeToDid, + cursor: cursor || undefined, + limit: limit || undefined, + }) + } else { + return getFollowSuggestionsGlobal(db, { + actorDid, + cursor: cursor || undefined, + limit: limit || undefined, + }) + } + }, +}) + +const getFollowSuggestionsGlobal = async ( + db: Database, + input: { actorDid: string; cursor?: string; limit?: number }, +) => { + const alreadyIncluded = parseCursor(input.cursor) + const suggestions = await db.db + .selectFrom('suggested_follow') + .innerJoin('actor', 'actor.did', 'suggested_follow.did') + .if(alreadyIncluded.length > 0, (qb) => + qb.where('suggested_follow.order', 'not in', alreadyIncluded), + ) + .selectAll() + .orderBy('suggested_follow.order', 'asc') + .execute() + + // always include first two + const firstTwo = suggestions.filter( + (row) => row.order === 1 || row.order === 2, + ) + const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2) + const limited = firstTwo.concat(shuffle(rest)).slice(0, input.limit) + + // if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle + const cursor = + limited.length > 0 + ? limited + .map((row) => row.order.toString()) + .concat(alreadyIncluded.map((id) => id.toString())) + .join(':') + : undefined + + return { + dids: limited.map((s) => s.did), + cursor, + } +} + +const getFollowSuggestionsRelativeTo = async ( + db: Database, + input: { + actorDid: string + relativeToDid: string + cursor?: string + limit?: number + }, +) => { + if (input.cursor) return { dids: [] } + const limit = input.limit ? Math.min(10, input.limit) : 10 + const actorsViewerFollows = db.db + .selectFrom('follow') + .where('creator', '=', input.actorDid) + .select('subjectDid') + const mostLikedAccounts = await db.db + .selectFrom( + db.db + .selectFrom('like') + .where('creator', '=', input.relativeToDid) + .select(sql`split_part(subject, '/', 3)`.as('subjectDid')) + .orderBy('sortAt', 'desc') + .limit(1000) // limit to 1000 + .as('likes'), + ) + .select('likes.subjectDid as did') + .select((qb) => qb.fn.count('likes.subjectDid').as('count')) + .where('likes.subjectDid', 'not in', actorsViewerFollows) + .where('likes.subjectDid', 'not in', [input.actorDid, input.relativeToDid]) + .groupBy('likes.subjectDid') + .orderBy('count', 'desc') + .limit(limit) + .execute() + const resultDids = mostLikedAccounts.map((a) => ({ did: a.did })) as { + did: string + }[] + + if (resultDids.length < limit) { + // backfill with popular accounts followed by actor + const mostPopularAccountsActorFollows = await db.db + .selectFrom('follow') + .innerJoin('profile_agg', 'follow.subjectDid', 'profile_agg.did') + .select('follow.subjectDid as did') + .where('follow.creator', '=', input.actorDid) + .where('follow.subjectDid', '!=', input.relativeToDid) + .where('follow.subjectDid', 'not in', actorsViewerFollows) + .if(resultDids.length > 0, (qb) => + qb.where( + 'subjectDid', + 'not in', + resultDids.map((a) => a.did), + ), + ) + .orderBy('profile_agg.followersCount', 'desc') + .limit(limit) + .execute() + + resultDids.push(...mostPopularAccountsActorFollows) + } + + if (resultDids.length < limit) { + // backfill with suggested_follow table + const additional = await db.db .selectFrom('suggested_follow') - .innerJoin('actor', 'actor.did', 'suggested_follow.did') - .if(alreadyIncluded.length > 0, (qb) => - qb.where('suggested_follow.order', 'not in', alreadyIncluded), + .where( + 'did', + 'not in', + // exclude any we already have + resultDids + .map((a) => a.did) + .concat([input.actorDid, input.relativeToDid]), ) + // and aren't already followed by viewer + .where('did', 'not in', actorsViewerFollows) .selectAll() - .orderBy('suggested_follow.order', 'asc') .execute() - // always include first two - const firstTwo = suggestions.filter( - (row) => row.order === 1 || row.order === 2, - ) - const rest = suggestions.filter((row) => row.order !== 1 && row.order !== 2) - const limited = firstTwo.concat(shuffle(rest)).slice(0, req.limit) - - // if the result set ends up getting larger, consider using a seed included in the cursor for for the randomized shuffle - const cursor = - limited.length > 0 - ? limited - .map((row) => row.order.toString()) - .concat(alreadyIncluded.map((id) => id.toString())) - .join(':') - : undefined - - return { - dids: suggestions.map((s) => s.did), - cursor, - } - }, -}) + resultDids.push(...additional) + } + + return { dids: resultDids.map((x) => x.did) } +} const parseCursor = (cursor?: string): number[] => { if (!cursor) { diff --git a/packages/bsky/src/data-plane/server/routes/threads.ts b/packages/bsky/src/data-plane/server/routes/threads.ts index 34c15b53fce..186c151b97e 100644 --- a/packages/bsky/src/data-plane/server/routes/threads.ts +++ b/packages/bsky/src/data-plane/server/routes/threads.ts @@ -1,8 +1,6 @@ import { ServiceImpl } from '@connectrpc/connect' import { Service } from '../../gen/bsky_connect' -import * as ui8 from 'uint8arrays' import { Database } from '../../../db' -import { keyBy } from '@atproto/common' import { getAncestorsAndSelfQb, getDescendentsQb, @@ -24,6 +22,8 @@ export default (db: Database): Partial> => ({ depth: below, }) .selectFrom('descendent') + .innerJoin('post', 'post.uri', 'descendent.uri') + .orderBy('post.sortAt', 'desc') .selectAll() .execute(), ]) @@ -33,22 +33,4 @@ export default (db: Database): Partial> => ({ ] return { uris } }, - - async getThreadgates(req) { - if (req.uris.length === 0) { - return { records: [] } - } - const res = await db.db - .selectFrom('record') - .selectAll() - .where('uri', 'in', req.uris) - .execute() - const byUri = keyBy(res, 'uri') - const records = req.uris.map((uri) => { - const row = byUri[uri] - const json = row ? row.json : JSON.stringify(null) - return ui8.fromString(json, 'utf8') - }) - return { records } - }, }) diff --git a/packages/bsky/src/db/pagination.ts b/packages/bsky/src/db/pagination.ts index d5887ae1fff..920f6658989 100644 --- a/packages/bsky/src/db/pagination.ts +++ b/packages/bsky/src/db/pagination.ts @@ -115,6 +115,15 @@ export class CreatedAtDidKeyset extends TimeCidKeyset<{ } } +export class IndexedAtDidKeyset extends TimeCidKeyset<{ + indexedAt: string + did: string // dids are treated identically to cids in TimeCidKeyset +}> { + labelResult(result: { indexedAt: string; did: string }) { + return { primary: result.indexedAt, secondary: result.did } + } +} + export const paginate = < QB extends AnyQb, K extends GenericKeyset, diff --git a/packages/bsky/src/feed-gen/best-of-follows.ts b/packages/bsky/src/feed-gen/best-of-follows.ts deleted file mode 100644 index 33c70ea81a4..00000000000 --- a/packages/bsky/src/feed-gen/best-of-follows.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { AlgoHandler, AlgoResponse } from './types' -import { GenericKeyset, paginate } from '../db/pagination' -import AppContext from '../context' - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - viewer: string | null, -): Promise => { - if (!viewer) { - throw new AuthRequiredError('This feed requires being logged-in') - } - - const { limit, cursor } = params - const db = ctx.db.getReplica('feed') - const feedService = ctx.services.feed(db) - const { ref } = db.db.dynamic - - // candidates are ranked within a materialized view by like count, depreciated over time. - - let builder = feedService - .selectPostQb() - .innerJoin('algo_whats_hot_view as candidate', 'candidate.uri', 'post.uri') - .where((qb) => - qb - .where('post.creator', '=', viewer) - .orWhereExists((inner) => - inner - .selectFrom('follow') - .where('follow.creator', '=', viewer) - .whereRef('follow.subjectDid', '=', 'post.creator'), - ), - ) - .select('candidate.score') - .select('candidate.cid') - - const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid')) - builder = paginate(builder, { limit, cursor, keyset }) - - const feedItems = await builder.execute() - - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler - -type Result = { score: number; cid: string } -type LabeledResult = { primary: number; secondary: string } -export class ScoreKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.score, - secondary: result.cid, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: Math.round(labeled.primary).toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const score = parseInt(cursor.primary, 10) - if (isNaN(score)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: score, - secondary: cursor.secondary, - } - } -} diff --git a/packages/bsky/src/feed-gen/bsky-team.ts b/packages/bsky/src/feed-gen/bsky-team.ts index feb9539345e..37e1cc317e5 100644 --- a/packages/bsky/src/feed-gen/bsky-team.ts +++ b/packages/bsky/src/feed-gen/bsky-team.ts @@ -31,11 +31,14 @@ const handler: AlgoHandler = async ( let feedQb = db.db.selectFrom(postsQb.as('feed_items')).selectAll() feedQb = paginate(feedQb, { limit, cursor, keyset }) - const feedItems = await feedQb.execute() - + const feedItemsRes = await feedQb.execute() + const feedItems = feedItemsRes.map((item) => ({ + itemUri: item.uri, + postUri: item.postUri, + })) return { feedItems, - cursor: keyset.packFromResult(feedItems), + cursor: keyset.packFromResult(feedItemsRes), } } diff --git a/packages/bsky/src/feed-gen/hot-classic.ts b/packages/bsky/src/feed-gen/hot-classic.ts index d1595105f27..4ddc449e66e 100644 --- a/packages/bsky/src/feed-gen/hot-classic.ts +++ b/packages/bsky/src/feed-gen/hot-classic.ts @@ -44,11 +44,15 @@ const handler: AlgoHandler = async ( let feedQb = db.db.selectFrom(postsQb.as('feed_items')).selectAll() feedQb = paginate(feedQb, { limit, cursor, keyset }) - const feedItems = await feedQb.execute() + const feedItemsRes = await feedQb.execute() + const feedItems = feedItemsRes.map((item) => ({ + itemUri: item.uri, + postUri: item.postUri, + })) return { feedItems, - cursor: keyset.packFromResult(feedItems), + cursor: keyset.packFromResult(feedItemsRes), } } diff --git a/packages/bsky/src/feed-gen/index.ts b/packages/bsky/src/feed-gen/index.ts index d00d22c59d9..5109d32416c 100644 --- a/packages/bsky/src/feed-gen/index.ts +++ b/packages/bsky/src/feed-gen/index.ts @@ -1,9 +1,7 @@ import { AtUri } from '@atproto/syntax' import { ids } from '../lexicon/lexicons' -import withFriends from './with-friends' import bskyTeam from './bsky-team' import hotClassic from './hot-classic' -import bestOfFollows from './best-of-follows' import mutuals from './mutuals' import { MountedAlgos } from './types' @@ -13,9 +11,7 @@ const feedgenUri = (did, name) => // These are custom algorithms that will be mounted directly onto an AppView // Feel free to remove, update to your own, or serve the following logic at a record that you control export const makeAlgos = (did: string): MountedAlgos => ({ - [feedgenUri(did, 'with-friends')]: withFriends, [feedgenUri(did, 'bsky-team')]: bskyTeam, [feedgenUri(did, 'hot-classic')]: hotClassic, - [feedgenUri(did, 'best-of-follows')]: bestOfFollows, [feedgenUri(did, 'mutuals')]: mutuals, }) diff --git a/packages/bsky/src/feed-gen/mutuals.ts b/packages/bsky/src/feed-gen/mutuals.ts index 86583ebaa56..24089818f8b 100644 --- a/packages/bsky/src/feed-gen/mutuals.ts +++ b/packages/bsky/src/feed-gen/mutuals.ts @@ -46,11 +46,15 @@ const handler: AlgoHandler = async ( feedQb = paginate(feedQb, { limit, cursor, keyset }) - const feedItems = await feedQb.execute() + const feedItemsRes = await feedQb.execute() + const feedItems = feedItemsRes.map((item) => ({ + itemUri: item.uri, + postUri: item.postUri, + })) return { feedItems, - cursor: keyset.packFromResult(feedItems), + cursor: keyset.packFromResult(feedItemsRes), } } diff --git a/packages/bsky/src/feed-gen/types.ts b/packages/bsky/src/feed-gen/types.ts index 4693d64d4dd..bf3eee202d0 100644 --- a/packages/bsky/src/feed-gen/types.ts +++ b/packages/bsky/src/feed-gen/types.ts @@ -1,10 +1,14 @@ import AppContext from '../context' import { SkeletonFeedPost } from '../lexicon/types/app/bsky/feed/defs' import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { FeedRow } from '../services/feed' + +export type AlgoResponseItem = { + itemUri: string + postUri: string +} export type AlgoResponse = { - feedItems: FeedRow[] + feedItems: AlgoResponseItem[] cursor?: string } @@ -17,15 +21,15 @@ export type AlgoHandler = ( export type MountedAlgos = Record export const toSkeletonItem = (feedItem: { - uri: string + itemUri: string postUri: string }): SkeletonFeedPost => ({ post: feedItem.postUri, reason: - feedItem.uri === feedItem.postUri + feedItem.itemUri === feedItem.postUri ? undefined : { $type: 'app.bsky.feed.defs#skeletonReasonRepost', - repost: feedItem.uri, + repost: feedItem.itemUri, }, }) diff --git a/packages/bsky/src/feed-gen/whats-hot.ts b/packages/bsky/src/feed-gen/whats-hot.ts deleted file mode 100644 index 2376b98f185..00000000000 --- a/packages/bsky/src/feed-gen/whats-hot.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { NotEmptyArray } from '@atproto/common' -import { InvalidRequestError } from '@atproto/xrpc-server' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { AlgoHandler, AlgoResponse } from './types' -import { GenericKeyset, paginate } from '../db/pagination' -import AppContext from '../context' -import { valuesList } from '../db/util' -import { sql } from 'kysely' -import { FeedItemType } from '../services/feed/types' - -const NO_WHATS_HOT_LABELS: NotEmptyArray = [ - '!no-promote', - 'corpse', - 'self-harm', - 'porn', - 'sexual', - 'nudity', - 'underwear', -] - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - _viewer: string | null, -): Promise => { - const { limit, cursor } = params - const db = ctx.db.getReplica('feed') - - const { ref } = db.db.dynamic - - // candidates are ranked within a materialized view by like count, depreciated over time. - - let builder = db.db - .selectFrom('algo_whats_hot_view as candidate') - .innerJoin('post', 'post.uri', 'candidate.uri') - .leftJoin('post_embed_record', 'post_embed_record.postUri', 'candidate.uri') - .whereNotExists((qb) => - qb - .selectFrom('label') - .selectAll() - .whereRef('val', 'in', valuesList(NO_WHATS_HOT_LABELS)) - .where('neg', '=', false) - .where((clause) => - clause - .whereRef('label.uri', '=', ref('post.creator')) - .orWhereRef('label.uri', '=', ref('post.uri')) - .orWhereRef('label.uri', '=', ref('post_embed_record.embedUri')), - ), - ) - .select([ - sql`${'post'}`.as('type'), - 'post.uri as uri', - 'post.uri as postUri', - 'post.creator as originatorDid', - 'post.creator as postAuthorDid', - 'post.replyParent as replyParent', - 'post.replyRoot as replyRoot', - 'post.indexedAt as sortAt', - 'candidate.score', - 'candidate.cid', - ]) - - const keyset = new ScoreKeyset(ref('candidate.score'), ref('candidate.cid')) - builder = paginate(builder, { limit, cursor, keyset }) - - const feedItems = await builder.execute() - - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler - -type Result = { score: number; cid: string } -type LabeledResult = { primary: number; secondary: string } -export class ScoreKeyset extends GenericKeyset { - labelResult(result: Result) { - return { - primary: result.score, - secondary: result.cid, - } - } - labeledResultToCursor(labeled: LabeledResult) { - return { - primary: Math.round(labeled.primary).toString(), - secondary: labeled.secondary, - } - } - cursorToLabeledResult(cursor: { primary: string; secondary: string }) { - const score = parseInt(cursor.primary, 10) - if (isNaN(score)) { - throw new InvalidRequestError('Malformed cursor') - } - return { - primary: score, - secondary: cursor.secondary, - } - } -} diff --git a/packages/bsky/src/feed-gen/with-friends.ts b/packages/bsky/src/feed-gen/with-friends.ts deleted file mode 100644 index 1e6d345ffcc..00000000000 --- a/packages/bsky/src/feed-gen/with-friends.ts +++ /dev/null @@ -1,43 +0,0 @@ -import AppContext from '../context' -import { QueryParams as SkeletonParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' -import { paginate } from '../db/pagination' -import { AlgoHandler, AlgoResponse } from './types' -import { FeedKeyset, getFeedDateThreshold } from '../api/app/bsky/util/feed' -import { AuthRequiredError } from '@atproto/xrpc-server' - -const handler: AlgoHandler = async ( - ctx: AppContext, - params: SkeletonParams, - viewer: string | null, -): Promise => { - if (!viewer) { - throw new AuthRequiredError('This feed requires being logged-in') - } - - const { cursor, limit = 50 } = params - const db = ctx.db.getReplica('feed') - const feedService = ctx.services.feed(db) - const { ref } = db.db.dynamic - - const keyset = new FeedKeyset(ref('post.sortAt'), ref('post.cid')) - const sortFrom = keyset.unpack(cursor)?.primary - - let postsQb = feedService - .selectPostQb() - .innerJoin('follow', 'follow.subjectDid', 'post.creator') - .innerJoin('post_agg', 'post_agg.uri', 'post.uri') - .where('post_agg.likeCount', '>=', 5) - .where('follow.creator', '=', viewer) - .where('post.sortAt', '>', getFeedDateThreshold(sortFrom)) - - postsQb = paginate(postsQb, { limit, cursor, keyset, tryIndex: true }) - - const feedItems = await postsQb.execute() - - return { - feedItems, - cursor: keyset.packFromResult(feedItems), - } -} - -export default handler diff --git a/packages/bsky/src/hydration/actor.ts b/packages/bsky/src/hydration/actor.ts new file mode 100644 index 00000000000..38c7096f891 --- /dev/null +++ b/packages/bsky/src/hydration/actor.ts @@ -0,0 +1,135 @@ +import { CID } from 'multiformats/cid' +import { DataPlaneClient } from '../data-plane/client' +import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile' +import { HydrationMap, parseCid, parseRecordBytes, parseString } from './util' + +export type Actor = { + did: string + handle?: string + profile?: ProfileRecord + profileCid?: CID + indexedAt?: Date + takendown: boolean +} + +export type Actors = HydrationMap + +export type ProfileViewerState = { + muted?: boolean + mutedByList?: string + blockedBy?: string + blocking?: string + blockedByList?: string + blockingByList?: string + following?: string + followedBy?: string +} + +export type ProfileViewerStates = HydrationMap + +export type ProfileAgg = { + followers: number + follows: number + posts: number +} + +export type ProfileAggs = HydrationMap + +export class ActorHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getRepoRevSafe(did: string | null): Promise { + if (!did) return null + try { + const res = await this.dataplane.getLatestRev({ actorDid: did }) + return parseString(res.rev) ?? null + } catch { + return null + } + } + + async getDids(handleOrDids: string[]): Promise<(string | undefined)[]> { + const handles = handleOrDids.filter((actor) => !actor.startsWith('did:')) + const res = await this.dataplane.getDidsByHandles({ handles }) + const didByHandle = handles.reduce((acc, cur, i) => { + const did = res.dids[i] + if (did && did.length > 0) { + return acc.set(cur, did) + } + return acc + }, new Map() as Map) + return handleOrDids.map((id) => + id.startsWith('did:') ? id : didByHandle.get(id), + ) + } + + async getDidsDefined(handleOrDids: string[]): Promise { + const res = await this.getDids(handleOrDids) + // @ts-ignore + return res.filter((did) => did !== undefined) + } + + async getActors(dids: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getActors({ dids }) + return dids.reduce((acc, did, i) => { + const actor = res.actors[i] + if (!actor.exists || (actor.takenDown && !includeTakedowns)) { + return acc.set(did, null) + } + const profile = + includeTakedowns || !actor.profile?.takenDown + ? actor.profile + : undefined + return acc.set(did, { + did, + handle: parseString(actor.handle), + profile: parseRecordBytes(profile?.record), + profileCid: parseCid(profile?.cid), + indexedAt: profile?.indexedAt?.toDate(), + takendown: actor.takenDown ?? false, + }) + }, new HydrationMap()) + } + + async getProfileViewerStates( + dids: string[], + viewer: string, + ): Promise { + const res = await this.dataplane.getRelationships({ + actorDid: viewer, + targetDids: dids, + }) + return dids.reduce((acc, did, i) => { + const rels = res.relationships[i] + if (viewer === did) { + // ignore self-follows, self-mutes, self-blocks + return acc.set(did, {}) + } + return acc.set(did, { + muted: rels.muted ?? false, + mutedByList: parseString(rels.mutedByList), + blockedBy: parseString(rels.blockedBy), + blocking: parseString(rels.blocking), + blockedByList: parseString(rels.blockedByList), + blockingByList: parseString(rels.blockingByList), + following: parseString(rels.following), + followedBy: parseString(rels.followedBy), + }) + }, new HydrationMap()) + } + + async getProfileAggregates(dids: string[]): Promise { + const [followers, follows, posts] = await Promise.all([ + this.dataplane.getFollowerCounts({ dids }), + this.dataplane.getFollowCounts({ dids }), + this.dataplane.getPostCounts({ dids }), + ]) + return dids.reduce((acc, did, i) => { + return acc.set(did, { + followers: followers.counts[i] ?? 0, + follows: follows.counts[i] ?? 0, + posts: posts.counts[i] ?? 0, + }) + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/feed.ts b/packages/bsky/src/hydration/feed.ts new file mode 100644 index 00000000000..ecb4defd6fd --- /dev/null +++ b/packages/bsky/src/hydration/feed.ts @@ -0,0 +1,178 @@ +import { DataPlaneClient } from '../data-plane/client' +import { Record as PostRecord } from '../lexicon/types/app/bsky/feed/post' +import { Record as LikeRecord } from '../lexicon/types/app/bsky/feed/like' +import { Record as RepostRecord } from '../lexicon/types/app/bsky/feed/repost' +import { Record as FeedGenRecord } from '../lexicon/types/app/bsky/feed/generator' +import { Record as ThreadgateRecord } from '../lexicon/types/app/bsky/feed/threadgate' +import { HydrationMap, RecordInfo, parseRecord, parseString } from './util' +import { AtUri } from '@atproto/syntax' +import { ids } from '../lexicon/lexicons' + +export type Post = RecordInfo & { violatesThreadGate: boolean } +export type Posts = HydrationMap + +export type PostViewerState = { + like?: string + repost?: string +} + +export type PostViewerStates = HydrationMap + +export type PostAgg = { + likes: number + replies: number + reposts: number +} + +export type PostAggs = HydrationMap + +export type Like = RecordInfo +export type Likes = HydrationMap + +export type Repost = RecordInfo +export type Reposts = HydrationMap + +export type FeedGenAgg = { + likes: number +} + +export type FeedGenAggs = HydrationMap + +export type FeedGen = RecordInfo +export type FeedGens = HydrationMap + +export type FeedGenViewerState = { + like?: string +} + +export type FeedGenViewerStates = HydrationMap + +export type Threadgate = RecordInfo +export type Threadgates = HydrationMap + +export class FeedHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getPosts(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getPostRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + const violatesThreadGate = res.meta[i].violatesThreadGate + return acc.set(uri, record ? { ...record, violatesThreadGate } : null) + }, new HydrationMap()) + } + + async getPostViewerStates( + uris: string[], + viewer: string, + ): Promise { + const [likes, reposts] = await Promise.all([ + this.dataplane.getLikesByActorAndSubjects({ + actorDid: viewer, + refs: uris.map((uri) => ({ uri })), + }), + this.dataplane.getRepostsByActorAndSubjects({ + actorDid: viewer, + refs: uris.map((uri) => ({ uri })), + }), + ]) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + like: parseString(likes.uris[i]), + repost: parseString(reposts.uris[i]), + }) + }, new HydrationMap()) + } + + async getPostAggregates(uris: string[]): Promise { + const [likes, reposts, replies] = await Promise.all([ + this.dataplane.getLikeCounts({ uris }), + this.dataplane.getRepostCounts({ uris }), + this.dataplane.getPostReplyCounts({ uris }), + ]) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + likes: likes.counts[i] ?? 0, + reposts: reposts.counts[i] ?? 0, + replies: replies.counts[i] ?? 0, + }) + }, new HydrationMap()) + } + + async getFeedGens( + uris: string[], + includeTakedowns = false, + ): Promise { + const res = await this.dataplane.getFeedGeneratorRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getFeedGenViewerStates( + uris: string[], + viewer: string, + ): Promise { + const likes = await this.dataplane.getLikesByActorAndSubjects({ + actorDid: viewer, + refs: uris.map((uri) => ({ uri })), + }) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + like: parseString(likes.uris[i]), + }) + }, new HydrationMap()) + } + + async getFeedGenAggregates(uris: string[]): Promise { + const likes = await this.dataplane.getLikeCounts({ uris }) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + likes: likes.counts[i] ?? 0, + }) + }, new HydrationMap()) + } + + async getThreadgatesForPosts( + postUris: string[], + includeTakedowns = false, + ): Promise { + const uris = postUris.map((uri) => { + const parsed = new AtUri(uri) + return AtUri.make( + parsed.hostname, + ids.AppBskyFeedThreadgate, + parsed.rkey, + ).toString() + }) + const res = await this.dataplane.getThreadGateRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + // @TODO may not be supported yet by data plane + async getLikes(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getLikeRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getReposts(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getRepostRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/graph.ts b/packages/bsky/src/hydration/graph.ts new file mode 100644 index 00000000000..68de79b7632 --- /dev/null +++ b/packages/bsky/src/hydration/graph.ts @@ -0,0 +1,187 @@ +import { Record as FollowRecord } from '../lexicon/types/app/bsky/graph/follow' +import { Record as BlockRecord } from '../lexicon/types/app/bsky/graph/block' +import { Record as ListRecord } from '../lexicon/types/app/bsky/graph/list' +import { Record as ListItemRecord } from '../lexicon/types/app/bsky/graph/listitem' +import { DataPlaneClient } from '../data-plane/client' +import { HydrationMap, RecordInfo, parseRecord } from './util' +import { FollowInfo } from '../data-plane/gen/bsky_pb' + +export type List = RecordInfo +export type Lists = HydrationMap + +export type ListItem = RecordInfo +export type ListItems = HydrationMap + +export type ListViewerState = { + viewerMuted?: string + viewerListBlockUri?: string + viewerInList?: string +} + +export type ListViewerStates = HydrationMap + +export type Follow = RecordInfo +export type Follows = HydrationMap + +export type Block = RecordInfo + +export type RelationshipPair = [didA: string, didB: string] + +const dedupePairs = (pairs: RelationshipPair[]): RelationshipPair[] => { + const mapped = pairs.reduce((acc, cur) => { + const sorted = cur.sort() + acc[sorted.join('-')] = sorted + return acc + }, {} as Record) + return Object.values(mapped) +} +export class Blocks { + _blocks: Map = new Map() + constructor() {} + + static key(didA: string, didB: string): string { + return [didA, didB].sort().join(',') + } + + set(didA: string, didB: string, exists: boolean): Blocks { + const key = Blocks.key(didA, didB) + this._blocks.set(key, exists) + return this + } + + has(didA: string, didB: string): boolean { + const key = Blocks.key(didA, didB) + return this._blocks.has(key) + } + + isBlocked(didA: string, didB: string): boolean { + const key = Blocks.key(didA, didB) + return this._blocks.get(key) ?? false + } + + merge(blocks: Blocks): Blocks { + blocks._blocks.forEach((exists, key) => { + this._blocks.set(key, exists) + }) + return this + } +} + +export class GraphHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getLists(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getListRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + // @TODO may not be supported yet by data plane + async getListItems( + uris: string[], + includeTakedowns = false, + ): Promise { + const res = await this.dataplane.getListItemRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord( + res.records[i], + includeTakedowns, + ) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getListViewerStates( + uris: string[], + viewer: string, + ): Promise { + const mutesAndBlocks = await Promise.all( + uris.map((uri) => this.getMutesAndBlocks(uri, viewer)), + ) + const listMemberships = await this.dataplane.getListMembership({ + actorDid: viewer, + listUris: uris, + }) + return uris.reduce((acc, uri, i) => { + return acc.set(uri, { + viewerMuted: mutesAndBlocks[i].muted ? uri : undefined, + viewerListBlockUri: mutesAndBlocks[i].listBlockUri || undefined, + viewerInList: listMemberships.listitemUris[i], + }) + }, new HydrationMap()) + } + + private async getMutesAndBlocks(uri: string, viewer: string) { + const [muted, listBlockUri] = await Promise.all([ + this.dataplane.getMutelistSubscription({ + actorDid: viewer, + listUri: uri, + }), + this.dataplane.getBlocklistSubscription({ + actorDid: viewer, + listUri: uri, + }), + ]) + return { + muted: muted.subscribed, + listBlockUri: listBlockUri.listblockUri, + } + } + + async getBidirectionalBlocks(pairs: RelationshipPair[]): Promise { + const deduped = dedupePairs(pairs).map(([a, b]) => ({ a, b })) + const res = await this.dataplane.getBlockExistence({ pairs: deduped }) + const blocks = new Blocks() + for (let i = 0; i < deduped.length; i++) { + const pair = deduped[i] + blocks.set(pair.a, pair.b, res.exists[i] ?? false) + } + return blocks + } + + async getFollows(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getFollowRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getBlocks(uris: string[], includeTakedowns = false): Promise { + const res = await this.dataplane.getBlockRecords({ uris }) + return uris.reduce((acc, uri, i) => { + const record = parseRecord(res.records[i], includeTakedowns) + return acc.set(uri, record ?? null) + }, new HydrationMap()) + } + + async getActorFollows(input: { + did: string + cursor?: string + limit?: number + }): Promise<{ follows: FollowInfo[]; cursor: string }> { + const { did, cursor, limit } = input + const res = await this.dataplane.getFollows({ + actorDid: did, + cursor, + limit, + }) + return { follows: res.follows, cursor: res.cursor } + } + + async getActorFollowers(input: { + did: string + cursor?: string + limit?: number + }): Promise<{ followers: FollowInfo[]; cursor: string }> { + const { did, cursor, limit } = input + const res = await this.dataplane.getFollowers({ + actorDid: did, + cursor, + limit, + }) + return { followers: res.followers, cursor: res.cursor } + } +} diff --git a/packages/bsky/src/hydration/hydrator.ts b/packages/bsky/src/hydration/hydrator.ts new file mode 100644 index 00000000000..ca1d3218630 --- /dev/null +++ b/packages/bsky/src/hydration/hydrator.ts @@ -0,0 +1,621 @@ +import assert from 'assert' +import { mapDefined } from '@atproto/common' +import { AtUri } from '@atproto/syntax' +import { DataPlaneClient } from '../data-plane/client' +import { Notification } from '../data-plane/gen/bsky_pb' +import { ids } from '../lexicon/lexicons' +import { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record' +import { isMain as isEmbedRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' +import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate' +import { + ActorHydrator, + ProfileAggs, + Actors, + ProfileViewerStates, + ProfileViewerState, +} from './actor' +import { + Follows, + GraphHydrator, + ListItems, + ListViewerStates, + Lists, + RelationshipPair, +} from './graph' +import { LabelHydrator, Labels } from './label' +import { HydrationMap, didFromUri, urisByCollection } from './util' +import { + FeedGenAggs, + FeedGens, + FeedGenViewerStates, + FeedHydrator, + Likes, + Post, + Posts, + Reposts, + PostAggs, + PostViewerStates, + Threadgates, +} from './feed' + +export type HydrationState = { + viewer?: string | null + actors?: Actors + profileViewers?: ProfileViewerStates + profileAggs?: ProfileAggs + posts?: Posts + postAggs?: PostAggs + postViewers?: PostViewerStates + postBlocks?: PostBlocks + reposts?: Reposts + follows?: Follows + followBlocks?: FollowBlocks + threadgates?: Threadgates + lists?: Lists + listViewers?: ListViewerStates + listItems?: ListItems + likes?: Likes + labels?: Labels + feedgens?: FeedGens + feedgenViewers?: FeedGenViewerStates + feedgenAggs?: FeedGenAggs +} + +export type PostBlock = { embed: boolean; reply: boolean } +export type PostBlocks = HydrationMap +type PostBlockPairs = { embed?: RelationshipPair; reply?: RelationshipPair } + +export type FollowBlock = boolean +export type FollowBlocks = HydrationMap + +export class Hydrator { + actor: ActorHydrator + feed: FeedHydrator + graph: GraphHydrator + label: LabelHydrator + + constructor(public dataplane: DataPlaneClient) { + this.actor = new ActorHydrator(dataplane) + this.feed = new FeedHydrator(dataplane) + this.graph = new GraphHydrator(dataplane) + this.label = new LabelHydrator(dataplane) + } + + // app.bsky.actor.defs#profileView + // - profile + // - list basic + async hydrateProfiles( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const [actors, labels, profileViewers] = await Promise.all([ + this.actor.getActors(dids, includeTakedowns), + this.label.getLabelsForSubjects(labelSubjectsForDid(dids)), + viewer ? this.actor.getProfileViewerStates(dids, viewer) : undefined, + ]) + const listUris: string[] = [] + profileViewers?.forEach((item) => { + listUris.push(...listUrisFromProfileViewer(item)) + }) + const listState = await this.hydrateListsBasic(listUris, viewer) + return mergeStates(listState, { + actors, + labels, + profileViewers, + viewer, + }) + } + + // app.bsky.actor.defs#profileViewBasic + // - profile basic + // - profile + // - list basic + async hydrateProfilesBasic( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + return this.hydrateProfiles(dids, viewer, includeTakedowns) + } + + // app.bsky.actor.defs#profileViewDetailed + // - profile detailed + // - profile + // - list basic + async hydrateProfilesDetailed( + dids: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const [state, profileAggs] = await Promise.all([ + this.hydrateProfiles(dids, viewer, includeTakedowns), + this.actor.getProfileAggregates(dids), + ]) + return { + ...state, + profileAggs, + } + } + + // app.bsky.graph.defs#listView + // - list + // - profile basic + async hydrateLists( + uris: string[], + viewer: string | null, + ): Promise { + const [listsState, profilesState] = await Promise.all([ + await this.hydrateListsBasic(uris, viewer), + await this.hydrateProfilesBasic(uris.map(didFromUri), viewer), + ]) + return mergeStates(listsState, profilesState) + } + + // app.bsky.graph.defs#listViewBasic + // - list basic + async hydrateListsBasic( + uris: string[], + viewer: string | null, + ): Promise { + const [lists, listViewers] = await Promise.all([ + this.graph.getLists(uris), + viewer ? this.graph.getListViewerStates(uris, viewer) : undefined, + ]) + return { lists, listViewers, viewer } + } + + // app.bsky.graph.defs#listItemView + // - list item + // - profile + // - list basic + async hydrateListItems( + uris: string[], + viewer: string | null, + ): Promise { + const listItems = await this.graph.getListItems(uris) + const dids: string[] = [] + listItems.forEach((item) => { + if (item) { + dids.push(item.record.subject) + } + }) + const profileState = await this.hydrateProfiles(dids, viewer) + return mergeStates(profileState, { listItems, viewer }) + } + + // app.bsky.feed.defs#postView + // - post + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + async hydratePosts( + uris: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const postsLayer0 = await this.feed.getPosts(uris, includeTakedowns) + // first level embeds plus thread roots we haven't fetched yet + const urisLayer1 = nestedRecordUrisFromPosts(postsLayer0) + const additionalRootUris = rootUrisFromPosts(postsLayer0) // supports computing threadgates + const urisLayer1ByCollection = urisByCollection(urisLayer1) + const postUrisLayer1 = urisLayer1ByCollection.get(ids.AppBskyFeedPost) ?? [] + const postsLayer1 = await this.feed.getPosts( + [...postUrisLayer1, ...additionalRootUris], + includeTakedowns, + ) + // second level embeds, ignoring any additional root uris we mixed-in to the previous layer + const urisLayer2 = nestedRecordUrisFromPosts(postsLayer1, postUrisLayer1) + const urisLayer2ByCollection = urisByCollection(urisLayer2) + const postUrisLayer2 = urisLayer2ByCollection.get(ids.AppBskyFeedPost) ?? [] + const threadRootUris = new Set() + for (const [uri, post] of postsLayer0) { + if (post) { + threadRootUris.add(rootUriFromPost(post) ?? uri) + } + } + const [postsLayer2, threadgates] = await Promise.all([ + this.feed.getPosts(postUrisLayer2, includeTakedowns), + this.feed.getThreadgatesForPosts([...threadRootUris.values()]), + ]) + // collect list/feedgen embeds, lists in threadgates, post record hydration + const gateListUris = getListUrisFromGates(threadgates) + const nestedListUris = [ + ...(urisLayer1ByCollection.get(ids.AppBskyGraphList) ?? []), + ...(urisLayer2ByCollection.get(ids.AppBskyGraphList) ?? []), + ] + const nestedFeedGenUris = [ + ...(urisLayer1ByCollection.get(ids.AppBskyFeedGenerator) ?? []), + ...(urisLayer2ByCollection.get(ids.AppBskyFeedGenerator) ?? []), + ] + const posts = + mergeManyMaps(postsLayer0, postsLayer1, postsLayer2) ?? postsLayer0 + const allPostUris = [...posts.keys()] + const [ + postAggs, + postViewers, + labels, + postBlocks, + profileState, + listState, + feedGenState, + ] = await Promise.all([ + this.feed.getPostAggregates(uris), + viewer ? this.feed.getPostViewerStates(uris, viewer) : undefined, + this.label.getLabelsForSubjects(allPostUris), + this.hydratePostBlocks(posts), + this.hydrateProfiles( + allPostUris.map(didFromUri), + viewer, + includeTakedowns, + ), + this.hydrateLists([...nestedListUris, ...gateListUris], viewer), + this.hydrateFeedGens(nestedFeedGenUris, viewer), + ]) + // combine all hydration state + return mergeManyStates(profileState, listState, feedGenState, { + posts, + postAggs, + postViewers, + postBlocks, + labels, + threadgates, + viewer, + }) + } + + private async hydratePostBlocks(posts: Posts): Promise { + const postBlocks = new HydrationMap() + const postBlocksPairs = new Map() + const relationships: RelationshipPair[] = [] + for (const [uri, item] of posts) { + if (!item) continue + const post = item.record + const creator = didFromUri(uri) + const postBlockPairs: PostBlockPairs = {} + postBlocksPairs.set(uri, postBlockPairs) + // 3p block for replies + const parentUri = post.reply?.parent.uri + const parentDid = parentUri && didFromUri(parentUri) + if (parentDid) { + const pair: RelationshipPair = [creator, parentDid] + relationships.push(pair) + postBlockPairs.reply = pair + } + // 3p block for record embeds + for (const embedUri of nestedRecordUris(post)) { + const pair: RelationshipPair = [creator, didFromUri(embedUri)] + relationships.push(pair) + postBlockPairs.embed = pair + } + } + // replace embed/reply pairs with block state + const blocks = await this.graph.getBidirectionalBlocks(relationships) + for (const [uri, { embed, reply }] of postBlocksPairs) { + postBlocks.set(uri, { + embed: !!embed && blocks.isBlocked(...embed), + reply: !!reply && blocks.isBlocked(...reply), + }) + } + return postBlocks + } + + // app.bsky.feed.defs#feedViewPost + // - post (+ replies) + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + // - repost + // - profile + // - list basic + // - post + // - ... + async hydrateFeedPosts( + uris: string[], + viewer: string | null, + includeTakedowns = false, + ): Promise { + const collectionUris = urisByCollection(uris) + const postUris = collectionUris.get(ids.AppBskyFeedPost) ?? [] + const repostUris = collectionUris.get(ids.AppBskyFeedRepost) ?? [] + const [posts, reposts, repostProfileState] = await Promise.all([ + this.feed.getPosts(postUris, includeTakedowns), + this.feed.getReposts(repostUris), + this.hydrateProfiles( + repostUris.map(didFromUri), + viewer, + includeTakedowns, + ), + ]) + const repostPostUris = mapDefined( + [...reposts.values()], + (repost) => repost?.record.subject.uri, + ) + const repostPosts = await this.feed.getPosts( + repostPostUris, + includeTakedowns, + ) + const repostedAndReplyUris: string[] = [] + repostPosts.forEach((post, uri) => { + repostedAndReplyUris.push(uri) + if (post?.record.reply) { + repostedAndReplyUris.push( + post.record.reply.root.uri, + post.record.reply.parent.uri, + ) + } + }) + posts.forEach((post) => { + if (post?.record.reply) { + repostedAndReplyUris.push( + post.record.reply.root.uri, + post.record.reply.parent.uri, + ) + } + }) + const postState = await this.hydratePosts( + [...postUris, ...repostedAndReplyUris], + viewer, + includeTakedowns, + ) + return mergeManyStates(postState, repostProfileState, { + reposts, + viewer, + }) + } + + // app.bsky.feed.defs#threadViewPost + // - post + // - profile + // - list basic + // - list + // - profile + // - list basic + // - feedgen + // - profile + // - list basic + async hydrateThreadPosts( + uris: string[], + viewer: string | null, + ): Promise { + return this.hydratePosts(uris, viewer) + } + + // app.bsky.feed.defs#generatorView + // - feedgen + // - profile + // - list basic + async hydrateFeedGens( + uris: string[], + viewer: string | null, + ): Promise { + const [feedgens, feedgenAggs, feedgenViewers, profileState] = + await Promise.all([ + this.feed.getFeedGens(uris), + this.feed.getFeedGenAggregates(uris), + viewer ? this.feed.getFeedGenViewerStates(uris, viewer) : undefined, + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { + feedgens, + feedgenAggs, + feedgenViewers, + viewer, + }) + } + + // app.bsky.feed.getLikes#like + // - like + // - profile + // - list basic + async hydrateLikes( + uris: string[], + viewer: string | null, + ): Promise { + const [likes, profileState] = await Promise.all([ + this.feed.getLikes(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { likes, viewer }) + } + + // app.bsky.feed.getRepostedBy#repostedBy + // - repost + // - profile + // - list basic + async hydrateReposts(uris: string[], viewer: string | null) { + const [reposts, profileState] = await Promise.all([ + this.feed.getReposts(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { reposts, viewer }) + } + + // app.bsky.notification.listNotifications#notification + // - notification + // - profile + // - list basic + async hydrateNotifications( + notifs: Notification[], + viewer: string | null, + ): Promise { + const uris = notifs.map((notif) => notif.uri) + const collections = urisByCollection(uris) + const postUris = collections.get(ids.AppBskyFeedPost) ?? [] + const likeUris = collections.get(ids.AppBskyFeedLike) ?? [] + const repostUris = collections.get(ids.AppBskyFeedRepost) ?? [] + const followUris = collections.get(ids.AppBskyGraphFollow) ?? [] + const [posts, likes, reposts, follows, labels, profileState] = + await Promise.all([ + this.feed.getPosts(postUris), // reason: mention, reply, quote + this.feed.getLikes(likeUris), // reason: like + this.feed.getReposts(repostUris), // reason: repost + this.graph.getFollows(followUris), // reason: follow + this.label.getLabelsForSubjects(uris), + this.hydrateProfiles(uris.map(didFromUri), viewer), + ]) + return mergeStates(profileState, { + posts, + likes, + reposts, + follows, + labels, + viewer, + }) + } + + // provides partial hydration state withing getFollows / getFollowers, mainly for applying rules + async hydrateFollows(uris: string[]): Promise { + const follows = await this.graph.getFollows(uris) + const pairs: RelationshipPair[] = [] + for (const [uri, follow] of follows) { + if (follow) { + pairs.push([didFromUri(uri), follow.record.subject]) + } + } + const blocks = await this.graph.getBidirectionalBlocks(pairs) + const followBlocks = new HydrationMap() + for (const [uri, follow] of follows) { + if (follow) { + followBlocks.set( + uri, + blocks.isBlocked(didFromUri(uri), follow.record.subject), + ) + } else { + followBlocks.set(uri, null) + } + } + return { follows, followBlocks } + } +} + +const listUrisFromProfileViewer = (item: ProfileViewerState | null) => { + const listUris: string[] = [] + if (item?.mutedByList) { + listUris.push(item.mutedByList) + } + if (item?.blockingByList) { + listUris.push(item.blockingByList) + } + return listUris +} + +const labelSubjectsForDid = (dids: string[]) => { + return [ + ...dids, + ...dids.map((did) => + AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(), + ), + ] +} + +const rootUrisFromPosts = (posts: Posts): string[] => { + const uris: string[] = [] + for (const item of posts.values()) { + const rootUri = item && rootUriFromPost(item) + if (rootUri) { + uris.push(rootUri) + } + } + return uris +} + +const rootUriFromPost = (post: Post): string | undefined => { + return post.record.reply?.root.uri +} + +const nestedRecordUrisFromPosts = ( + posts: Posts, + fromUris?: string[], +): string[] => { + const uris: string[] = [] + const postUris = fromUris ?? posts.keys() + for (const uri of postUris) { + const item = posts.get(uri) + if (item) { + uris.push(...nestedRecordUris(item.record)) + } + } + return uris +} + +const nestedRecordUris = (post: Post['record']): string[] => { + const uris: string[] = [] + if (!post?.embed) return uris + if (isEmbedRecord(post.embed)) { + uris.push(post.embed.record.uri) + } else if (isEmbedRecordWithMedia(post.embed)) { + uris.push(post.embed.record.record.uri) + } + return uris +} + +const getListUrisFromGates = (gates: Threadgates) => { + const uris: string[] = [] + for (const gate of gates.values()) { + const listRules = gate?.record.allow?.filter(isListRule) ?? [] + for (const rule of listRules) { + uris.push(rule.list) + } + } + return uris +} + +export const mergeStates = ( + stateA: HydrationState, + stateB: HydrationState, +): HydrationState => { + assert( + !stateA.viewer || !stateB.viewer || stateA.viewer === stateB.viewer, + 'incompatible viewers', + ) + return { + viewer: stateA.viewer ?? stateB.viewer, + actors: mergeMaps(stateA.actors, stateB.actors), + profileAggs: mergeMaps(stateA.profileAggs, stateB.profileAggs), + profileViewers: mergeMaps(stateA.profileViewers, stateB.profileViewers), + posts: mergeMaps(stateA.posts, stateB.posts), + postAggs: mergeMaps(stateA.postAggs, stateB.postAggs), + postViewers: mergeMaps(stateA.postViewers, stateB.postViewers), + postBlocks: mergeMaps(stateA.postBlocks, stateB.postBlocks), + reposts: mergeMaps(stateA.reposts, stateB.reposts), + follows: mergeMaps(stateA.follows, stateB.follows), + followBlocks: mergeMaps(stateA.followBlocks, stateB.followBlocks), + threadgates: mergeMaps(stateA.threadgates, stateB.threadgates), + lists: mergeMaps(stateA.lists, stateB.lists), + listViewers: mergeMaps(stateA.listViewers, stateB.listViewers), + listItems: mergeMaps(stateA.listItems, stateB.listItems), + likes: mergeMaps(stateA.likes, stateB.likes), + labels: mergeMaps(stateA.labels, stateB.labels), + feedgens: mergeMaps(stateA.feedgens, stateB.feedgens), + feedgenAggs: mergeMaps(stateA.feedgenAggs, stateB.feedgenAggs), + feedgenViewers: mergeMaps(stateA.feedgenViewers, stateB.feedgenViewers), + } +} + +const mergeMaps = ( + mapA?: HydrationMap, + mapB?: HydrationMap, +): HydrationMap | undefined => { + if (!mapA) return mapB + if (!mapB) return mapA + return mapA.merge(mapB) +} + +const mergeManyStates = (...states: HydrationState[]) => { + return states.reduce(mergeStates, {} as HydrationState) +} + +const mergeManyMaps = (...maps: HydrationMap[]) => { + return maps.reduce(mergeMaps, undefined as HydrationMap | undefined) +} diff --git a/packages/bsky/src/hydration/label.ts b/packages/bsky/src/hydration/label.ts new file mode 100644 index 00000000000..5910dc77988 --- /dev/null +++ b/packages/bsky/src/hydration/label.ts @@ -0,0 +1,29 @@ +import { DataPlaneClient } from '../data-plane/client' +import { Label } from '../lexicon/types/com/atproto/label/defs' +import { HydrationMap, parseJsonBytes } from './util' + +export type { Label } from '../lexicon/types/com/atproto/label/defs' + +export type Labels = HydrationMap + +export class LabelHydrator { + constructor(public dataplane: DataPlaneClient) {} + + async getLabelsForSubjects( + subjects: string[], + issuers?: string[], + ): Promise { + const res = await this.dataplane.getLabels({ subjects, issuers }) + return res.labels.reduce((acc, cur) => { + const label = parseJsonBytes(cur) as Label | undefined + if (!label) return acc + const entry = acc.get(label.uri) + if (entry) { + entry.push(label) + } else { + acc.set(label.uri, [label]) + } + return acc + }, new HydrationMap()) + } +} diff --git a/packages/bsky/src/hydration/util.ts b/packages/bsky/src/hydration/util.ts new file mode 100644 index 00000000000..afbb9250914 --- /dev/null +++ b/packages/bsky/src/hydration/util.ts @@ -0,0 +1,78 @@ +import { AtUri } from '@atproto/syntax' +import { jsonToLex } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import * as ui8 from 'uint8arrays' +import { Record } from '../data-plane/gen/bsky_pb' + +export class HydrationMap extends Map { + merge(map: HydrationMap): HydrationMap { + map.forEach((val, key) => { + this.set(key, val) + }) + return this + } +} + +export type RecordInfo = { + record: T + cid: CID + indexedAt?: Date + takenDown: boolean +} + +export const parseRecord = ( + entry: Record, + includeTakedowns: boolean, +): RecordInfo | undefined => { + if (!includeTakedowns && entry.takenDown) { + return undefined + } + const record = parseRecordBytes(entry.record) + const cid = parseCid(entry.cid) + const indexedAt = entry.indexedAt?.toDate() + if (!record || !cid) return + return { record, cid, indexedAt, takenDown: entry.takenDown } +} + +export const parseRecordBytes = ( + bytes: Uint8Array | undefined, +): T | undefined => { + const parsed = parseJsonBytes(bytes) + return jsonToLex(parsed) as T +} + +export const parseJsonBytes = ( + bytes: Uint8Array | undefined, +): JSON | undefined => { + if (!bytes || bytes.byteLength === 0) return + const parsed = JSON.parse(ui8.toString(bytes, 'utf8')) + return parsed ?? undefined +} + +export const parseString = (str: string | undefined): string | undefined => { + return str && str.length > 0 ? str : undefined +} + +export const parseCid = (cidStr: string | undefined): CID | undefined => { + if (!cidStr || cidStr.length === 0) return + try { + return CID.parse(cidStr) + } catch { + return + } +} + +export const didFromUri = (uri: string) => { + return new AtUri(uri).hostname +} + +export const urisByCollection = (uris: string[]): Map => { + const result = new Map() + for (const uri of uris) { + const collection = new AtUri(uri).collection + const items = result.get(collection) ?? [] + items.push(uri) + result.set(collection, items) + } + return result +} diff --git a/packages/bsky/src/index.ts b/packages/bsky/src/index.ts index 7ceba61f990..55ca62353f9 100644 --- a/packages/bsky/src/index.ts +++ b/packages/bsky/src/index.ts @@ -27,7 +27,11 @@ import { LabelCache } from './label-cache' import { NotificationServer } from './notifications' import { AtpAgent } from '@atproto/api' import { Keypair } from '@atproto/crypto' +import { createDataPlaneClient } from './data-plane/client' +import { Hydrator } from './hydration/hydrator' +import { Views } from './views' +export * from './data-plane' export type { ServerConfigValues } from './config' export type { MountedAlgos } from './feed-gen/types' export { ServerConfig } from './config' @@ -115,10 +119,17 @@ export class BskyAppView { labelCache, }) + const dataplane = createDataPlaneClient(config.dataplaneUrl, '1.1') + const hydrator = new Hydrator(dataplane) + const views = new Views(imgUriBuilder) + const ctx = new AppContext({ db, cfg: config, services, + dataplane, + hydrator, + views, imgUriBuilder, signingKey, idResolver, diff --git a/packages/bsky/src/pipeline.ts b/packages/bsky/src/pipeline.ts index 7798519bfa2..50f1abfd566 100644 --- a/packages/bsky/src/pipeline.ts +++ b/packages/bsky/src/pipeline.ts @@ -1,22 +1,48 @@ -export function createPipeline< - Params, - SkeletonState, - HydrationState extends SkeletonState, - View, - Context, ->( - skeleton: (params: Params, ctx: Context) => Promise, - hydration: (state: SkeletonState, ctx: Context) => Promise, - rules: (state: HydrationState, ctx: Context) => HydrationState, - presentation: (state: HydrationState, ctx: Context) => View, +import { HydrationState } from './hydration/hydrator' + +export function createPipeline( + skeletonFn: (input: SkeletonFnInput) => Promise, + hydrationFn: ( + input: HydrationFnInput, + ) => Promise, + rulesFn: (input: RulesFnInput) => Skeleton, + presentationFn: ( + input: PresentationFnInput, + ) => View, ) { return async (params: Params, ctx: Context) => { - const skeletonState = await skeleton(params, ctx) - const hydrationState = await hydration(skeletonState, ctx) - return presentation(rules(hydrationState, ctx), ctx) + const skeleton = await skeletonFn({ ctx, params }) + const hydration = await hydrationFn({ ctx, params, skeleton }) + const appliedRules = rulesFn({ ctx, params, skeleton, hydration }) + return presentationFn({ ctx, params, skeleton: appliedRules, hydration }) } } -export function noRules(state: T) { - return state +export type SkeletonFnInput = { + ctx: Context + params: Params +} + +export type HydrationFnInput = { + ctx: Context + params: Params + skeleton: Skeleton +} + +export type RulesFnInput = { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +} + +export type PresentationFnInput = { + ctx: Context + params: Params + skeleton: Skeleton + hydration: HydrationState +} + +export function noRules(input: { skeleton: S }) { + return input.skeleton } diff --git a/packages/bsky/src/views/index.ts b/packages/bsky/src/views/index.ts new file mode 100644 index 00000000000..67b93c859ee --- /dev/null +++ b/packages/bsky/src/views/index.ts @@ -0,0 +1,844 @@ +import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax' +import { ImageUriBuilder } from '../image/uri' +import { HydrationState } from '../hydration/hydrator' +import { ids } from '../lexicon/lexicons' +import { + ProfileViewDetailed, + ProfileView, + ProfileViewBasic, + ViewerState as ProfileViewerState, +} from '../lexicon/types/app/bsky/actor/defs' +import { + BlockedPost, + FeedViewPost, + GeneratorView, + NotFoundPost, + PostView, + ReasonRepost, + ThreadViewPost, + ThreadgateView, +} from '../lexicon/types/app/bsky/feed/defs' +import { ListView, ListViewBasic } from '../lexicon/types/app/bsky/graph/defs' +import { compositeTime, creatorFromUri } from './util' +import { mapDefined } from '@atproto/common' +import { isListRule } from '../lexicon/types/app/bsky/feed/threadgate' +import { isSelfLabels } from '../lexicon/types/com/atproto/label/defs' +import { + Embed, + EmbedBlocked, + EmbedNotFound, + EmbedView, + ExternalEmbed, + ExternalEmbedView, + ImagesEmbed, + ImagesEmbedView, + MaybePostView, + NotificationView, + PostEmbedView, + RecordEmbed, + RecordEmbedView, + RecordEmbedViewInternal, + RecordWithMedia, + RecordWithMediaView, + isExternalEmbed, + isImagesEmbed, + isRecordEmbed, + isRecordWithMedia, +} from './types' +import { Label } from '../hydration/label' +import { Repost } from '../hydration/feed' +import { RecordInfo } from '../hydration/util' +import { Notification } from '../data-plane/gen/bsky_pb' +import { parseThreadGate } from '../services/feed/util' + +export class Views { + constructor(public imgUriBuilder: ImageUriBuilder) {} + + // Actor + // ------------ + + actorIsTakendown(did: string, state: HydrationState): boolean { + return state.actors?.get(did)?.takendown ?? false + } + + viewerBlockExists(did: string, state: HydrationState): boolean { + const actor = state.profileViewers?.get(did) + if (!actor) return false + return ( + !!actor.blockedBy || + !!actor.blocking || + !!actor.blockedByList || + !!actor.blockingByList + ) + } + + viewerMuteExists(did: string, state: HydrationState): boolean { + const actor = state.profileViewers?.get(did) + if (!actor) return false + return actor.muted || !!actor.mutedByList + } + + profileDetailed( + did: string, + state: HydrationState, + ): ProfileViewDetailed | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const baseView = this.profile(did, state) + if (!baseView) return + const profileAggs = state.profileAggs?.get(did) + return { + ...baseView, + banner: actor.profile?.banner + ? this.imgUriBuilder.getPresetUri( + 'banner', + did, + actor.profile.banner.ref, + ) + : undefined, + followersCount: profileAggs?.followers, + followsCount: profileAggs?.follows, + postsCount: profileAggs?.posts, + } + } + + profile(did: string, state: HydrationState): ProfileView | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const basicView = this.profileBasic(did, state) + if (!basicView) return + return { + ...basicView, + description: actor.profile?.description || undefined, + indexedAt: actor.indexedAt?.toISOString(), + } + } + + profileBasic( + did: string, + state: HydrationState, + ): ProfileViewBasic | undefined { + const actor = state.actors?.get(did) + if (!actor) return + const profileUri = AtUri.make( + did, + ids.AppBskyActorProfile, + 'self', + ).toString() + const labels = [ + ...(state.labels?.get(did) ?? []), + ...(state.labels?.get(profileUri) ?? []), + ...this.selfLabels({ + uri: profileUri, + cid: actor.profileCid?.toString(), + record: actor.profile, + }), + ] + return { + did, + handle: actor.handle ?? INVALID_HANDLE, + displayName: actor.profile?.displayName, + avatar: actor.profile?.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + did, + actor.profile.avatar.ref, + ) + : undefined, + viewer: this.profileViewer(did, state), + labels, + } + } + + profileViewer( + did: string, + state: HydrationState, + ): ProfileViewerState | undefined { + const viewer = state.profileViewers?.get(did) + if (!viewer) return + const blockedByUri = viewer.blockedBy || viewer.blockedByList + const blockingUri = viewer.blocking || viewer.blockingByList + const block = !!blockedByUri || !!blockingUri + return { + muted: viewer.muted || !!viewer.mutedByList, + mutedByList: viewer.mutedByList + ? this.listBasic(viewer.mutedByList, state) + : undefined, + blockedBy: !!blockedByUri, + blocking: blockingUri, + blockingByList: viewer.blockingByList + ? this.listBasic(viewer.blockingByList, state) + : undefined, + following: viewer.following && !block ? viewer.following : undefined, + followedBy: viewer.followedBy && !block ? viewer.followedBy : undefined, + } + } + + blockedProfileViewer( + did: string, + state: HydrationState, + ): ProfileViewerState | undefined { + const viewer = state.profileViewers?.get(did) + if (!viewer) return + const blockedByUri = viewer.blockedBy || viewer.blockedByList + const blockingUri = viewer.blocking || viewer.blockingByList + return { + blockedBy: !!blockedByUri, + blocking: blockingUri, + } + } + + // Graph + // ------------ + + list(uri: string, state: HydrationState): ListView | undefined { + const creatorDid = new AtUri(uri).hostname + const list = state.lists?.get(uri) + if (!list) return + const creator = this.profile(creatorDid, state) + if (!creator) return + const basicView = this.listBasic(uri, state) + if (!basicView) return + + return { + ...basicView, + creator, + description: list.record.description, + descriptionFacets: list.record.descriptionFacets, + indexedAt: compositeTime( + normalizeDatetimeAlways(list.record.createdAt), + list.indexedAt?.toISOString(), + ), + } + } + + listBasic(uri: string, state: HydrationState): ListViewBasic | undefined { + const list = state.lists?.get(uri) + if (!list) { + return undefined + } + const listViewer = state.listViewers?.get(uri) + const creator = new AtUri(uri).hostname + return { + uri, + cid: list.cid.toString(), + name: list.record.name, + purpose: list.record.purpose, + avatar: list.record.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + creator, + list.record.avatar.ref, + ) + : undefined, + indexedAt: compositeTime( + normalizeDatetimeAlways(list.record.createdAt), + list.indexedAt?.toISOString(), + ), + viewer: listViewer + ? { + muted: !!listViewer.viewerMuted, + blocked: listViewer.viewerListBlockUri, + } + : undefined, + } + } + + // Labels + // ------------ + + selfLabels(details: { + uri?: string + cid?: string + record?: Record + }): Label[] { + const { uri, cid, record } = details + if (!uri || !cid || !record) return [] + if (!isSelfLabels(record.labels)) return [] + const src = new AtUri(uri).host // record creator + const cts = + typeof record.createdAt === 'string' + ? normalizeDatetimeAlways(record.createdAt) + : new Date(0).toISOString() + return record.labels.values.map(({ val }) => { + return { src, uri, cid, val, cts, neg: false } + }) + } + + // Feed + // ------------ + + feedItemBlocksAndMutes( + uri: string, + state: HydrationState, + ): { + originatorMuted: boolean + originatorBlocked: boolean + authorMuted: boolean + authorBlocked: boolean + } { + const parsed = new AtUri(uri) + if (parsed.collection === ids.AppBskyFeedRepost) { + const repost = state.reposts?.get(uri) + const postUri = repost?.record.subject.uri + const postDid = postUri ? creatorFromUri(postUri) : undefined + return { + originatorMuted: this.viewerMuteExists(parsed.hostname, state), + originatorBlocked: this.viewerBlockExists(parsed.hostname, state), + authorMuted: !!postDid && this.viewerMuteExists(postDid, state), + authorBlocked: !!postDid && this.viewerBlockExists(postDid, state), + } + } else { + return { + originatorMuted: this.viewerMuteExists(parsed.hostname, state), + originatorBlocked: this.viewerBlockExists(parsed.hostname, state), + authorMuted: this.viewerMuteExists(parsed.hostname, state), + authorBlocked: this.viewerBlockExists(parsed.hostname, state), + } + } + } + + feedGenerator(uri: string, state: HydrationState): GeneratorView | undefined { + const feedgen = state.feedgens?.get(uri) + if (!feedgen) return + const creatorDid = creatorFromUri(uri) + const creator = this.profile(creatorDid, state) + if (!creator) return + const viewer = state.feedgenViewers?.get(uri) + const aggs = state.feedgenAggs?.get(uri) + + return { + uri, + cid: feedgen.cid.toString(), + did: feedgen.record.did, + creator, + displayName: feedgen.record.displayName, + description: feedgen.record.description, + descriptionFacets: feedgen.record.descriptionFacets, + avatar: feedgen.record.avatar + ? this.imgUriBuilder.getPresetUri( + 'avatar', + creatorDid, + feedgen.record.avatar.ref, + ) + : undefined, + likeCount: aggs?.likes, + viewer: viewer + ? { + like: viewer.like, + } + : undefined, + indexedAt: compositeTime( + normalizeDatetimeAlways(feedgen.record.createdAt), + feedgen.indexedAt?.toISOString(), + ), + } + } + + threadGate(uri: string, state: HydrationState): ThreadgateView | undefined { + const gate = state.threadgates?.get(uri) + if (!gate) return + return { + uri, + cid: gate.cid.toString(), + record: gate.record, + lists: mapDefined(gate.record.allow ?? [], (rule) => { + if (!isListRule(rule)) return + return this.listBasic(rule.list, state) + }), + } + } + + post(uri: string, state: HydrationState, depth = 0): PostView | undefined { + const post = state.posts?.get(uri) + if (!post) return + const parsedUri = new AtUri(uri) + const authorDid = parsedUri.hostname + const author = this.profileBasic(authorDid, state) + if (!author) return + const aggs = state.postAggs?.get(uri) + const viewer = state.postViewers?.get(uri) + const gateUri = AtUri.make( + authorDid, + ids.AppBskyFeedThreadgate, + parsedUri.rkey, + ).toString() + const labels = [ + ...(state.labels?.get(uri) ?? []), + ...this.selfLabels({ + uri, + cid: post.cid.toString(), + record: post.record, + }), + ] + return { + uri, + cid: post.cid.toString(), + author, + record: post.record, + embed: + depth < 2 && post.record.embed + ? this.embed(uri, post.record.embed, state, depth + 1) + : undefined, + replyCount: aggs?.replies, + repostCount: aggs?.reposts, + likeCount: aggs?.likes, + indexedAt: (post.indexedAt ?? new Date()).toISOString(), + viewer: viewer + ? { + repost: viewer.repost, + like: viewer.like, + replyDisabled: this.userReplyDisabled(uri, state), + } + : undefined, + labels, + threadgate: !post.record.reply // only hydrate gate on root post + ? this.threadGate(gateUri, state) + : undefined, + } + } + + feedViewPost(uri: string, state: HydrationState): FeedViewPost | undefined { + // no block violating posts in feeds + if (state.postBlocks?.get(uri)?.reply) return undefined + const parsedUri = new AtUri(uri) + const postInfo = state.posts?.get(uri) + let postUri: AtUri + let reason: ReasonRepost | undefined + if (parsedUri.collection === ids.AppBskyFeedRepost) { + const repost = state.reposts?.get(uri) + if (!repost) return + reason = this.reasonRepost(parsedUri.hostname, repost, state) + if (!reason) return + postUri = new AtUri(repost.record.subject.uri) + } else { + postUri = parsedUri + } + const post = this.post(postUri.toString(), state) + if (!post) return + return { + post, + reason, + reply: !postInfo?.violatesThreadGate + ? this.replyRef(postUri.toString(), state) + : undefined, + } + } + + replyRef(uri: string, state: HydrationState, usePostViewUnion = false) { + const postRecord = state.posts?.get(uri.toString())?.record + if (!postRecord?.reply) return + const root = this.maybePost( + postRecord.reply.root.uri, + state, + usePostViewUnion, + ) + const parent = this.maybePost( + postRecord.reply.parent.uri, + state, + usePostViewUnion, + ) + return root && parent ? { root, parent } : undefined + } + + maybePost( + uri: string, + state: HydrationState, + usePostViewUnion = false, + ): MaybePostView | undefined { + const post = this.post(uri, state) + if (!post) return usePostViewUnion ? this.notFoundPost(uri) : undefined + if (this.viewerBlockExists(post.author.did, state)) { + return usePostViewUnion + ? this.blockedPost(uri, post.author.did, state) + : undefined + } + return { + $type: 'app.bsky.feed.defs#postView', + ...post, + } + } + + blockedPost( + uri: string, + authorDid: string, + state: HydrationState, + ): BlockedPost { + return { + $type: 'app.bsky.feed.defs#blockedPost', + uri, + blocked: true, + author: { + did: authorDid, + viewer: this.blockedProfileViewer(authorDid, state), + }, + } + } + + notFoundPost(uri: string): NotFoundPost { + return { + $type: 'app.bsky.feed.defs#notFoundPost', + uri, + notFound: true, + } + } + + reasonRepost( + creatorDid: string, + repost: Repost, + state: HydrationState, + ): ReasonRepost | undefined { + const creator = this.profileBasic(creatorDid, state) + if (!creator) return + if (!repost.indexedAt) return + return { + $type: 'app.bsky.feed.defs#reasonRepost', + by: creator, + indexedAt: repost.indexedAt.toISOString(), + } + } + + // Threads + // ------------ + + thread( + skele: { anchor: string; uris: string[] }, + state: HydrationState, + opts: { height: number; depth: number }, + ): ThreadViewPost | NotFoundPost | BlockedPost { + const { anchor, uris } = skele + const post = this.post(anchor, state) + if (!post) return this.notFoundPost(anchor) + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(anchor, post.author.did, state) + } + const includedPosts = new Set([anchor]) + const childrenByParentUri: Record = {} + uris.forEach((uri) => { + const post = state.posts?.get(uri) + const parentUri = post?.record.reply?.parent.uri + if (!parentUri) return + if (includedPosts.has(uri)) return + includedPosts.add(uri) + childrenByParentUri[parentUri] ??= [] + childrenByParentUri[parentUri].push(uri) + }) + const violatesThreadGate = state.posts?.get(anchor)?.violatesThreadGate + + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + parent: !violatesThreadGate + ? this.threadParent(anchor, state, opts.height) + : undefined, + replies: !violatesThreadGate + ? this.threadReplies(anchor, childrenByParentUri, state, opts.depth) + : undefined, + } + } + + threadParent( + childUri: string, + state: HydrationState, + height: number, + ): ThreadViewPost | NotFoundPost | BlockedPost | undefined { + if (height < 1) return undefined + const parentUri = state.posts?.get(childUri)?.record.reply?.parent.uri + if (!parentUri) return undefined + if (state.postBlocks?.get(childUri)?.reply) { + return this.blockedPost(parentUri, creatorFromUri(parentUri), state) + } + const post = this.post(parentUri, state) + if (!post) return this.notFoundPost(parentUri) + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(parentUri, post.author.did, state) + } + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + parent: this.threadParent(parentUri, state, height - 1), + } + } + + threadReplies( + parentUri: string, + childrenByParentUri: Record, + state: HydrationState, + depth: number, + ): (ThreadViewPost | NotFoundPost | BlockedPost)[] | undefined { + if (depth < 1) return undefined + const childrenUris = childrenByParentUri[parentUri] ?? [] + return mapDefined(childrenUris, (uri) => { + if (state.posts?.get(uri)?.violatesThreadGate) { + return undefined + } + if (state.postBlocks?.get(uri)?.reply) { + return undefined + } + const post = this.post(uri, state) + if (!post) return this.notFoundPost(parentUri) + if (this.viewerBlockExists(post.author.did, state)) { + return this.blockedPost(parentUri, post.author.did, state) + } + return { + $type: 'app.bsky.feed.defs#threadViewPost', + post, + replies: this.threadReplies(uri, childrenByParentUri, state, depth - 1), + } + }) + } + + // Embeds + // ------------ + + embed( + postUri: string, + embed: Embed | { $type: string }, + state: HydrationState, + depth: number, + ): EmbedView | undefined { + if (isImagesEmbed(embed)) { + return this.imagesEmbed(creatorFromUri(postUri), embed) + } else if (isExternalEmbed(embed)) { + return this.externalEmbed(creatorFromUri(postUri), embed) + } else if (isRecordEmbed(embed)) { + return this.recordEmbed(postUri, embed, state, depth) + } else if (isRecordWithMedia(embed)) { + return this.recordWithMediaEmbed(postUri, embed, state, depth) + } else { + return undefined + } + } + + imagesEmbed(did: string, embed: ImagesEmbed): ImagesEmbedView { + const imgViews = embed.images.map((img) => ({ + thumb: this.imgUriBuilder.getPresetUri( + 'feed_thumbnail', + did, + img.image.ref, + ), + fullsize: this.imgUriBuilder.getPresetUri( + 'feed_fullsize', + did, + img.image.ref, + ), + alt: img.alt, + aspectRatio: img.aspectRatio, + })) + return { + $type: 'app.bsky.embed.images#view', + images: imgViews, + } + } + + externalEmbed(did: string, embed: ExternalEmbed): ExternalEmbedView { + const { uri, title, description, thumb } = embed.external + return { + $type: 'app.bsky.embed.external#view', + external: { + uri, + title, + description, + thumb: thumb + ? this.imgUriBuilder.getPresetUri('feed_thumbnail', did, thumb.ref) + : undefined, + }, + } + } + + embedNotFound(uri: string): { $type: string; record: EmbedNotFound } { + return { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewNotFound', + uri, + notFound: true, + }, + } + } + + embedBlocked( + uri: string, + state: HydrationState, + ): { $type: string; record: EmbedBlocked } { + const creator = creatorFromUri(uri) + return { + $type: 'app.bsky.embed.record#view', + record: { + $type: 'app.bsky.embed.record#viewBlocked', + uri, + blocked: true, + author: { + did: creator, + viewer: this.blockedProfileViewer(creator, state), + }, + }, + } + } + + embedPostView( + uri: string, + state: HydrationState, + depth: number, + ): PostEmbedView | undefined { + const postView = this.post(uri, state, depth) + if (!postView) return + return { + $type: 'app.bsky.embed.record#viewRecord', + uri: postView.uri, + cid: postView.cid, + author: postView.author, + value: postView.record, + labels: postView.labels, + indexedAt: postView.indexedAt, + embeds: depth > 1 ? undefined : postView.embed ? [postView.embed] : [], + } + } + + recordEmbed( + postUri: string, + embed: RecordEmbed, + state: HydrationState, + depth: number, + withTypeTag = true, + ): RecordEmbedView { + const uri = embed.record.uri + const parsedUri = new AtUri(uri) + if ( + this.viewerBlockExists(parsedUri.hostname, state) || + state.postBlocks?.get(postUri)?.embed + ) { + return this.embedBlocked(uri, state) + } + + if (parsedUri.collection === ids.AppBskyFeedPost) { + const view = this.embedPostView(uri, state, depth) + if (!view) return this.embedNotFound(uri) + return this.recordEmbedWrapper(view, withTypeTag) + } else if (parsedUri.collection === ids.AppBskyFeedGenerator) { + const view = this.feedGenerator(uri, state) + if (!view) return this.embedNotFound(uri) + view.$type = 'app.bsky.feed.defs#generatorView' + return this.recordEmbedWrapper(view, withTypeTag) + } else if (parsedUri.collection === ids.AppBskyGraphList) { + const view = this.list(uri, state) + if (!view) return this.embedNotFound(uri) + view.$type = 'app.bsky.graph.defs#listView' + return this.recordEmbedWrapper(view, withTypeTag) + } + return this.embedNotFound(uri) + } + + private recordEmbedWrapper( + record: RecordEmbedViewInternal, + withTypeTag: boolean, + ): RecordEmbedView { + return { + $type: withTypeTag ? 'app.bsky.embed.record#view' : undefined, + record, + } + } + + recordWithMediaEmbed( + postUri: string, + embed: RecordWithMedia, + state: HydrationState, + depth: number, + ): RecordWithMediaView | undefined { + const creator = creatorFromUri(postUri) + let mediaEmbed: ImagesEmbedView | ExternalEmbedView + if (isImagesEmbed(embed.media)) { + mediaEmbed = this.imagesEmbed(creator, embed.media) + } else if (isExternalEmbed(embed.media)) { + mediaEmbed = this.externalEmbed(creator, embed.media) + } else { + return + } + return { + $type: 'app.bsky.embed.recordWithMedia#view', + media: mediaEmbed, + record: this.recordEmbed(postUri, embed.record, state, depth, false), + } + } + + userReplyDisabled(uri: string, state: HydrationState): boolean | undefined { + const post = state.posts?.get(uri) + if (post?.violatesThreadGate) { + return true + } + const rootUriStr: string = post?.record.reply?.root.uri ?? uri + const gate = state.threadgates?.get(postToGateUri(rootUriStr))?.record + if (!gate || !state.viewer) { + return undefined + } + const rootPost = state.posts?.get(rootUriStr)?.record + const ownerDid = new AtUri(rootUriStr).hostname + const { + canReply, + allowFollowing, + allowListUris = [], + } = parseThreadGate(state.viewer, ownerDid, rootPost ?? null, gate) + if (canReply) { + return false + } + if (allowFollowing && state.profileViewers?.get(ownerDid)?.followedBy) { + return false + } + for (const listUri of allowListUris) { + const list = state.listViewers?.get(listUri) + if (list?.viewerInList) { + return false + } + } + return true + } + + notification( + notif: Notification, + lastSeenAt: string | undefined, + state: HydrationState, + ): NotificationView | undefined { + if (!notif.timestamp || !notif.reason) return + const uri = new AtUri(notif.uri) + const authorDid = uri.hostname + const author = this.profile(authorDid, state) + if (!author) return + let recordInfo: RecordInfo> | null | undefined + if (uri.collection === ids.AppBskyFeedPost) { + recordInfo = state.posts?.get(notif.uri) + } else if (uri.collection === ids.AppBskyFeedLike) { + recordInfo = state.likes?.get(notif.uri) + } else if (uri.collection === ids.AppBskyFeedRepost) { + recordInfo = state.reposts?.get(notif.uri) + } else if (uri.collection === ids.AppBskyGraphFollow) { + recordInfo = state.follows?.get(notif.uri) + } + if (!recordInfo) return + const labels = state.labels?.get(notif.uri) ?? [] + const selfLabels = this.selfLabels({ + uri: notif.uri, + cid: recordInfo.cid.toString(), + record: recordInfo.record, + }) + const indexedAt = notif.timestamp.toDate().toISOString() + return { + uri: notif.uri, + cid: recordInfo.cid.toString(), + author, + reason: notif.reason, + reasonSubject: notif.reasonSubject || undefined, + record: recordInfo.record, + isRead: lastSeenAt ? lastSeenAt >= indexedAt : false, + indexedAt: notif.timestamp.toDate().toISOString(), + labels: [...labels, ...selfLabels], + } + } +} + +const postToGateUri = (uri: string) => { + const aturi = new AtUri(uri) + if (aturi.collection === ids.AppBskyFeedPost) { + aturi.collection = ids.AppBskyFeedThreadgate + } + return aturi.toString() +} diff --git a/packages/bsky/src/views/types.ts b/packages/bsky/src/views/types.ts new file mode 100644 index 00000000000..8c5a3deb026 --- /dev/null +++ b/packages/bsky/src/views/types.ts @@ -0,0 +1,72 @@ +import { + Main as ImagesEmbed, + View as ImagesEmbedView, +} from '../lexicon/types/app/bsky/embed/images' +import { + Main as ExternalEmbed, + View as ExternalEmbedView, +} from '../lexicon/types/app/bsky/embed/external' +import { + Main as RecordEmbed, + View as RecordEmbedView, + ViewBlocked as EmbedBlocked, + ViewNotFound as EmbedNotFound, + ViewRecord as PostEmbedView, +} from '../lexicon/types/app/bsky/embed/record' +import { + Main as RecordWithMedia, + View as RecordWithMediaView, +} from '../lexicon/types/app/bsky/embed/recordWithMedia' +import { + BlockedPost, + GeneratorView, + NotFoundPost, + PostView, +} from '../lexicon/types/app/bsky/feed/defs' +import { ListView } from '../lexicon/types/app/bsky/graph/defs' + +export type { + Main as ImagesEmbed, + View as ImagesEmbedView, +} from '../lexicon/types/app/bsky/embed/images' +export { isMain as isImagesEmbed } from '../lexicon/types/app/bsky/embed/images' +export type { + Main as ExternalEmbed, + View as ExternalEmbedView, +} from '../lexicon/types/app/bsky/embed/external' +export { isMain as isExternalEmbed } from '../lexicon/types/app/bsky/embed/external' +export type { + Main as RecordEmbed, + View as RecordEmbedView, + ViewBlocked as EmbedBlocked, + ViewNotFound as EmbedNotFound, + ViewRecord as PostEmbedView, +} from '../lexicon/types/app/bsky/embed/record' +export { isMain as isRecordEmbed } from '../lexicon/types/app/bsky/embed/record' +export type { + Main as RecordWithMedia, + View as RecordWithMediaView, +} from '../lexicon/types/app/bsky/embed/recordWithMedia' +export { isMain as isRecordWithMedia } from '../lexicon/types/app/bsky/embed/recordWithMedia' +export type { View as RecordWithMediaEmbedView } from '../lexicon/types/app/bsky/embed/recordWithMedia' +export type { + BlockedPost, + GeneratorView, + NotFoundPost, + PostView, +} from '../lexicon/types/app/bsky/feed/defs' +export type { ListView } from '../lexicon/types/app/bsky/graph/defs' + +export type { Notification as NotificationView } from '../lexicon/types/app/bsky/notification/listNotifications' + +export type Embed = ImagesEmbed | ExternalEmbed | RecordEmbed | RecordWithMedia + +export type EmbedView = + | ImagesEmbedView + | ExternalEmbedView + | RecordEmbedView + | RecordWithMediaView + +export type MaybePostView = PostView | NotFoundPost | BlockedPost + +export type RecordEmbedViewInternal = PostEmbedView | GeneratorView | ListView diff --git a/packages/bsky/src/views/util.ts b/packages/bsky/src/views/util.ts new file mode 100644 index 00000000000..3b01d5b64ce --- /dev/null +++ b/packages/bsky/src/views/util.ts @@ -0,0 +1,13 @@ +import { AtUri } from '@atproto/syntax' + +const now = () => { + return new Date().toISOString() +} + +export const compositeTime = (createdAt = now(), indexedAt = now()): string => { + return createdAt < indexedAt ? createdAt : indexedAt +} + +export const creatorFromUri = (uri: string): string => { + return new AtUri(uri).hostname +} diff --git a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap index ac9e0eee7a0..299d7323529 100644 --- a/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap +++ b/packages/bsky/tests/__snapshots__/feed-generation.test.ts.snap @@ -65,9 +65,11 @@ Object { "cid": "cids(2)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", + "description": "its me!", "did": "user(3)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(3)", @@ -938,13 +940,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", }, ], }, @@ -952,8 +954,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1223,13 +1225,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(4)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(4)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(4)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(4)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(5)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(5)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(5)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(5)@jpeg", }, ], }, @@ -1237,8 +1239,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(1)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(1)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1457,9 +1459,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -1505,9 +1509,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -1533,23 +1539,23 @@ Object { "muted": false, }, }, - "description": "Provides all feed candidates", + "description": "Provides even-indexed feed candidates", "did": "user(0)", - "displayName": "All", + "displayName": "Even", "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 2, + "likeCount": 0, "uri": "record(0)", - "viewer": Object { - "like": "record(4)", - }, + "viewer": Object {}, }, Object { "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -1575,13 +1581,15 @@ Object { "muted": false, }, }, - "description": "Provides even-indexed feed candidates", + "description": "Provides all feed candidates", "did": "user(0)", - "displayName": "Even", + "displayName": "All", "indexedAt": "1970-01-01T00:00:00.000Z", - "likeCount": 0, - "uri": "record(5)", - "viewer": Object {}, + "likeCount": 2, + "uri": "record(4)", + "viewer": Object { + "like": "record(5)", + }, }, ], } @@ -1594,9 +1602,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -1636,9 +1646,11 @@ Object { "cid": "cids(3)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -1676,9 +1688,11 @@ Object { "cid": "cids(4)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", diff --git a/packages/bsky/tests/admin/repo-search.test.ts b/packages/bsky/tests/admin/repo-search.test.ts index 837c4b2154a..9e643ba12e0 100644 --- a/packages/bsky/tests/admin/repo-search.test.ts +++ b/packages/bsky/tests/admin/repo-search.test.ts @@ -17,6 +17,7 @@ describe('admin repo search view', () => { sc = network.getSeedClient() await usersBulkSeed(sc) headers = network.pds.adminAuthHeaders() + await network.processAll() }) afterAll(async () => { diff --git a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap index a2549b0a52c..f7a61d82547 100644 --- a/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/author-feed.test.ts.snap @@ -746,13 +746,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -760,8 +760,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -1037,13 +1037,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -1051,8 +1051,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], diff --git a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap index 7f0989a5975..e5e068353ea 100644 --- a/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/block-lists.test.ts.snap @@ -263,11 +263,11 @@ Object { "did": "user(2)", "viewer": Object { "blockedBy": false, - "blocking": "record(6)", + "blocking": "record(5)", }, }, "blocked": true, - "uri": "record(5)", + "uri": "record(0)", }, Object { "$type": "app.bsky.feed.defs#blockedPost", @@ -275,11 +275,11 @@ Object { "did": "user(3)", "viewer": Object { "blockedBy": false, - "blocking": "record(6)", + "blocking": "record(5)", }, }, "blocked": true, - "uri": "record(7)", + "uri": "record(0)", }, ], }, diff --git a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap index 2a27fcf4955..079cdbe60a5 100644 --- a/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/blocks.test.ts.snap @@ -263,11 +263,11 @@ Object { "did": "user(2)", "viewer": Object { "blockedBy": false, - "blocking": "record(6)", + "blocking": "record(5)", }, }, "blocked": true, - "uri": "record(5)", + "uri": "record(0)", }, Object { "$type": "app.bsky.feed.defs#threadViewPost", @@ -280,7 +280,7 @@ Object { "labels": Array [], "viewer": Object { "blockedBy": false, - "following": "record(8)", + "following": "record(7)", "muted": false, }, }, @@ -302,7 +302,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(6)", "val": "test-label", }, Object { @@ -310,7 +310,7 @@ Object { "cts": "1970-01-01T00:00:00.000Z", "neg": false, "src": "did:example:labeler", - "uri": "record(7)", + "uri": "record(6)", "val": "test-label-2", }, ], @@ -348,7 +348,7 @@ Object { }, "replyCount": 1, "repostCount": 0, - "uri": "record(7)", + "uri": "record(6)", "viewer": Object {}, }, }, diff --git a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap index d4b11f0d235..a7b34c07dc0 100644 --- a/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/mute-lists.test.ts.snap @@ -38,9 +38,11 @@ Object { "cid": "cids(4)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(1)/cids(1)@jpeg", + "description": "its me!", "did": "user(0)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", diff --git a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap index fb0fd6a3224..85c3493e092 100644 --- a/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/thread.test.ts.snap @@ -54,7 +54,6 @@ Object { "like": "record(7)", }, }, - "replies": Array [], }, "post": Object { "author": Object { @@ -135,7 +134,6 @@ Object { "uri": "record(5)", "viewer": Object {}, }, - "replies": Array [], }, "post": Object { "author": Object { @@ -1073,6 +1071,11 @@ Object { }, }, "replies": Array [ + Object { + "$type": "app.bsky.feed.defs#notFoundPost", + "notFound": true, + "uri": "record(0)", + }, Object { "$type": "app.bsky.feed.defs#threadViewPost", "post": Object { diff --git a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap index b5863382fef..eb8934f00bb 100644 --- a/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap +++ b/packages/bsky/tests/views/__snapshots__/timeline.test.ts.snap @@ -2898,13 +2898,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -2912,8 +2912,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3112,8 +3112,8 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3128,8 +3128,8 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, ], }, @@ -3377,8 +3377,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3393,8 +3393,8 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, ], }, @@ -3739,8 +3739,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3833,13 +3833,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -3847,8 +3847,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -3947,8 +3947,8 @@ Array [ Object { "post": Object { "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4088,13 +4088,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -4102,8 +4102,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4304,8 +4304,8 @@ Array [ "parent": Object { "$type": "app.bsky.feed.defs#postView", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], @@ -4321,8 +4321,8 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(4)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(4)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", }, ], }, @@ -4815,13 +4815,13 @@ Array [ "images": Array [ Object { "alt": "tests/sample-img/key-landscape-small.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(2)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(2)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(2)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(2)@jpeg", }, Object { "alt": "tests/sample-img/key-alt.jpg", - "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(5)/cids(3)@jpeg", - "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(5)/cids(3)@jpeg", + "fullsize": "https://bsky.public.url/img/feed_fullsize/plain/user(3)/cids(3)@jpeg", + "thumb": "https://bsky.public.url/img/feed_thumbnail/plain/user(3)/cids(3)@jpeg", }, ], }, @@ -4829,8 +4829,8 @@ Array [ "record": Object { "$type": "app.bsky.embed.record#viewRecord", "author": Object { - "avatar": "https://bsky.public.url/img/avatar/plain/user(4)/cids(5)@jpeg", - "did": "user(3)", + "avatar": "https://bsky.public.url/img/avatar/plain/user(5)/cids(5)@jpeg", + "did": "user(4)", "displayName": "bobby", "handle": "bob.test", "labels": Array [], diff --git a/packages/bsky/tests/views/blocks.test.ts b/packages/bsky/tests/views/blocks.test.ts index 5d344a823d9..19986d02739 100644 --- a/packages/bsky/tests/views/blocks.test.ts +++ b/packages/bsky/tests/views/blocks.test.ts @@ -398,10 +398,9 @@ describe('pds views with blocking', () => { { headers: await network.serviceHeaders(alice) }, ) assert(isThreadViewPost(unblock.thread)) - expect(unblock.thread.replies?.map(getThreadPostUri)).toEqual([ - carolReplyToDan.ref.uriStr, - aliceReplyToDan.ref.uriStr, - ]) + expect(unblock.thread.replies?.map(getThreadPostUri).sort()).toEqual( + [aliceReplyToDan.ref.uriStr, carolReplyToDan.ref.uriStr].sort(), + ) // block then reply danBlockCarol = await pdsAgent.api.app.bsky.graph.block.create( diff --git a/packages/bsky/tests/views/follows.test.ts b/packages/bsky/tests/views/follows.test.ts index f290ec622d5..38d08bc5633 100644 --- a/packages/bsky/tests/views/follows.test.ts +++ b/packages/bsky/tests/views/follows.test.ts @@ -19,7 +19,6 @@ describe('pds follow views', () => { sc = network.getSeedClient() await followsSeed(sc) await network.processAll() - await network.bsky.processAll() alice = sc.dids.alice }) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index 07a6690f910..f16d59cd405 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -93,7 +93,6 @@ describe('bsky views with mutes from mute lists', () => { }) it('uses a list for mutes', async () => { - // @TODO proxy through appview await agent.api.app.bsky.graph.muteActorList( { list: listUri, diff --git a/packages/bsky/tests/views/thread.test.ts b/packages/bsky/tests/views/thread.test.ts index f13be284a30..0f31c8b8815 100644 --- a/packages/bsky/tests/views/thread.test.ts +++ b/packages/bsky/tests/views/thread.test.ts @@ -31,7 +31,6 @@ describe('pds thread views', () => { // Add a repost of a reply so that we can confirm myState in the thread await sc.repost(bob, sc.replies[alice][0].ref) await network.processAll() - await network.bsky.processAll() }) afterAll(async () => { @@ -165,22 +164,21 @@ describe('pds thread views', () => { describe('takedown', () => { it('blocks post by actor', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: alice, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: alice, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) // Same as shallow post thread test, minus alice const promise = agent.api.app.bsky.feed.getPostThread( @@ -211,22 +209,21 @@ describe('pds thread views', () => { }) it('blocks replies by actor', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: carol, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: carol, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) // Same as deep post thread test, minus carol const thread = await agent.api.app.bsky.feed.getPostThread( @@ -255,22 +252,21 @@ describe('pds thread views', () => { }) it('blocks ancestors by actor', async () => { - const { data: modAction } = - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.admin.defs#repoRef', - did: bob, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: bob, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) // Same as ancestor post thread test, minus bob const thread = await agent.api.app.bsky.feed.getPostThread( @@ -300,23 +296,22 @@ describe('pds thread views', () => { it('blocks post by record', async () => { const postRef = sc.posts[alice][1].ref - const { data: modAction } = - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: postRef.uriStr, - cid: postRef.cidStr, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: postRef.uriStr, + cid: postRef.cidStr, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) const promise = agent.api.app.bsky.feed.getPostThread( { depth: 1, uri: postRef.uriStr }, @@ -354,23 +349,22 @@ describe('pds thread views', () => { const parent = threadPreTakedown.data.thread.parent?.['post'] - const { data: modAction } = - await agent.api.com.atproto.admin.emitModerationEvent( - { - event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, - subject: { - $type: 'com.atproto.repo.strongRef', - uri: parent.uri, - cid: parent.cid, - }, - createdBy: 'did:example:admin', - reason: 'Y', - }, - { - encoding: 'application/json', - headers: network.pds.adminAuthHeaders(), + await agent.api.com.atproto.admin.emitModerationEvent( + { + event: { $type: 'com.atproto.admin.defs#modEventTakedown' }, + subject: { + $type: 'com.atproto.repo.strongRef', + uri: parent.uri, + cid: parent.cid, }, - ) + createdBy: 'did:example:admin', + reason: 'Y', + }, + { + encoding: 'application/json', + headers: network.pds.adminAuthHeaders(), + }, + ) // Same as ancestor post thread test, minus parent post const thread = await agent.api.app.bsky.feed.getPostThread( @@ -407,7 +401,7 @@ describe('pds thread views', () => { const post1 = threadPreTakedown.data.thread.replies?.[0].post const post2 = threadPreTakedown.data.thread.replies?.[1].replies[0].post - const actionResults = await Promise.all( + await Promise.all( [post1, post2].map((post) => agent.api.com.atproto.admin.emitModerationEvent( { diff --git a/packages/bsky/tests/views/threadgating.test.ts b/packages/bsky/tests/views/threadgating.test.ts index c0667bcf874..0dd9343d61b 100644 --- a/packages/bsky/tests/views/threadgating.test.ts +++ b/packages/bsky/tests/views/threadgating.test.ts @@ -309,8 +309,9 @@ describe('views with thread gating', () => { assert(isThreadViewPost(reply1)) assert(isThreadViewPost(reply2)) expect(otherReplies.length).toEqual(0) - expect(reply1.post.uri).toEqual(danReply.ref.uriStr) - expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + expect([reply1.post.uri, reply2.post.uri].sort()).toEqual( + [danReply.ref.uriStr, aliceReply.ref.uriStr].sort(), + ) }) it('applies gate for unknown list rule.', async () => { @@ -419,8 +420,9 @@ describe('views with thread gating', () => { assert(isThreadViewPost(reply1)) assert(isThreadViewPost(reply2)) expect(otherReplies.length).toEqual(0) - expect(reply1.post.uri).toEqual(danReply.ref.uriStr) - expect(reply2.post.uri).toEqual(aliceReply.ref.uriStr) + expect([reply1.post.uri, reply2.post.uri].sort()).toEqual( + [aliceReply.ref.uriStr, danReply.ref.uriStr].sort(), + ) }) it('applies gate for missing rules, takes no action.', async () => { diff --git a/packages/dev-env/src/bsky.ts b/packages/dev-env/src/bsky.ts index 8320130eb43..7c29ef4e86f 100644 --- a/packages/dev-env/src/bsky.ts +++ b/packages/dev-env/src/bsky.ts @@ -18,6 +18,7 @@ export class TestBsky { public server: bsky.BskyAppView, public indexer: bsky.BskyIndexer, public ingester: bsky.BskyIngester, + public dataplane: bsky.DataPlaneServer, ) {} static async create(cfg: BskyConfig): Promise { @@ -34,6 +35,22 @@ export class TestBsky { signer: serviceKeypair, }) + // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. + const db = new bsky.DatabaseCoordinator({ + schema: cfg.dbPostgresSchema, + primary: { + url: cfg.dbPrimaryPostgresUrl, + poolSize: 10, + }, + replicas: [], + }) + + const dataplanePort = await getPort() + const dataplane = await bsky.DataPlaneServer.create( + db.getPrimary(), + dataplanePort, + ) + const config = new bsky.ServerConfig({ version: '0.0.0', port, @@ -42,6 +59,7 @@ export class TestBsky { serverDid, didCacheStaleTTL: HOUR, didCacheMaxTTL: DAY, + dataplaneUrl: `http://localhost:${dataplanePort}`, ...cfg, // Each test suite gets its own lock id for the repo subscription adminPassword: ADMIN_PASSWORD, @@ -51,16 +69,6 @@ export class TestBsky { feedGenDid: 'did:example:feedGen', }) - // shared across server, ingester, and indexer in order to share pool, avoid too many pg connections. - const db = new bsky.DatabaseCoordinator({ - schema: cfg.dbPostgresSchema, - primary: { - url: cfg.dbPrimaryPostgresUrl, - poolSize: 10, - }, - replicas: [], - }) - // Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..." const migrationDb = new bsky.PrimaryDatabase({ url: cfg.dbPrimaryPostgresUrl, @@ -146,7 +154,7 @@ export class TestBsky { // we refresh label cache by hand in `processAll` instead of on a timer server.ctx.labelCache.stop() - return new TestBsky(url, port, server, indexer, ingester) + return new TestBsky(url, port, server, indexer, ingester, dataplane) } get ctx(): bsky.AppContext { @@ -190,6 +198,7 @@ export class TestBsky { async close() { await this.server.destroy({ skipDb: true }) + await this.dataplane.destroy() await this.ingester.destroy({ skipDb: true }) await this.indexer.destroy() // closes shared db } diff --git a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap index f856407ccbc..7d7d4325dcf 100644 --- a/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap +++ b/packages/pds/tests/proxied/__snapshots__/views.test.ts.snap @@ -114,7 +114,7 @@ Object { }, }, ], - "cursor": "1:3", + "cursor": "1:2:3", } `; @@ -204,6 +204,24 @@ Array [ "did": "user(0)", "displayName": "ali", "handle": "alice.test", + "labels": Array [ + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-a", + }, + Object { + "cid": "cids(1)", + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "user(0)", + "uri": "record(0)", + "val": "self-label-b", + }, + ], "viewer": Object { "blockedBy": false, "muted": false, @@ -214,29 +232,40 @@ Array [ "did": "user(2)", "displayName": "bobby", "handle": "bob.test", + "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(1)", - "following": "record(0)", + "followedBy": "record(2)", + "following": "record(1)", "muted": false, }, }, Object { "did": "user(4)", "handle": "carol.test", + "labels": Array [], "viewer": Object { "blockedBy": false, - "followedBy": "record(3)", - "following": "record(2)", + "followedBy": "record(4)", + "following": "record(3)", "muted": false, }, }, Object { "did": "user(5)", "handle": "dan.test", + "labels": Array [ + Object { + "cts": "1970-01-01T00:00:00.000Z", + "neg": false, + "src": "did:example:labeler", + "uri": "user(5)", + "val": "repo-action-label", + }, + ], "viewer": Object { "blockedBy": false, - "following": "record(4)", + "following": "record(5)", "muted": false, }, }, @@ -496,9 +525,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", @@ -539,9 +570,11 @@ Object { "cid": "cids(0)", "creator": Object { "avatar": "https://bsky.public.url/img/avatar/plain/user(2)/cids(1)@jpeg", + "description": "its me!", "did": "user(1)", "displayName": "ali", "handle": "alice.test", + "indexedAt": "1970-01-01T00:00:00.000Z", "labels": Array [ Object { "cid": "cids(2)", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb994c1ab4b..1fc12a3e242 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,11 +36,11 @@ importers: specifier: ^18.0.0 version: 18.0.0 '@typescript-eslint/eslint-plugin': - specifier: ^5.38.1 - version: 5.38.1(@typescript-eslint/parser@5.38.1)(eslint@8.24.0)(typescript@4.8.4) + specifier: ^6.14.0 + version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.24.0)(typescript@5.3.3) '@typescript-eslint/parser': - specifier: ^5.38.1 - version: 5.38.1(eslint@8.24.0)(typescript@4.8.4) + specifier: ^6.14.0 + version: 6.14.0(eslint@8.24.0)(typescript@5.3.3) babel-eslint: specifier: ^10.1.0 version: 10.1.0(eslint@8.24.0) @@ -82,10 +82,10 @@ importers: version: 5.0.0(prettier@2.7.1) ts-node: specifier: ^10.8.2 - version: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + version: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@5.3.3) typescript: - specifier: ^4.8.4 - version: 4.8.4 + specifier: ^5.3.3 + version: 5.3.3 packages/api: dependencies: @@ -422,7 +422,7 @@ importers: devDependencies: ts-node: specifier: ^10.8.1 - version: 10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.9.5) + version: 10.8.2(@swc/core@1.3.42)(@types/node@20.10.4)(typescript@5.3.3) packages/identity: dependencies: @@ -803,7 +803,7 @@ importers: version: 4.20.0 opentelemetry-plugin-better-sqlite3: specifier: ^1.1.0 - version: 1.1.0(better-sqlite3@7.6.2) + version: 1.1.0(better-sqlite3@9.2.2) packages: @@ -874,7 +874,7 @@ packages: p-queue: 6.6.2 pg: 8.10.0 pino: 8.15.0 - pino-http: 8.4.0 + pino-http: 8.2.1 sharp: 0.31.3 typed-emitter: 2.1.0 uint8arrays: 3.0.0 @@ -4886,6 +4886,21 @@ packages: - pg-native - supports-color + /@eslint-community/eslint-utils@4.4.0(eslint@8.24.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.24.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@1.4.1: resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -5721,8 +5736,8 @@ packages: pretty-format: 28.1.3 dev: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true /@types/mime@1.3.2: @@ -5746,6 +5761,12 @@ packages: /@types/node@18.17.8: resolution: {integrity: sha512-Av/7MqX/iNKwT9Tr60V85NqMnsmh8ilfJoBlIVibkXfitk9Q22D9Y5mSpm+FvG5DET7EbVfB40bOiLzKgYFgPw==} + /@types/node@20.10.4: + resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} + dependencies: + undici-types: 5.26.5 + dev: true + /@types/nodemailer@6.4.6: resolution: {integrity: sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==} dependencies: @@ -5778,6 +5799,10 @@ packages: resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} dev: true + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + /@types/send@0.17.1: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: @@ -5821,129 +5846,134 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@5.38.1(@typescript-eslint/parser@5.38.1)(eslint@8.24.0)(typescript@4.8.4): - resolution: {integrity: sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.24.0)(typescript@5.3.3): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.38.1(eslint@8.24.0)(typescript@4.8.4) - '@typescript-eslint/scope-manager': 5.38.1 - '@typescript-eslint/type-utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) - '@typescript-eslint/utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.14.0(eslint@8.24.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.24.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.24.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4 eslint: 8.24.0 - ignore: 5.2.4 - regexpp: 3.2.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 semver: 7.5.4 - tsutils: 3.21.0(typescript@4.8.4) - typescript: 4.8.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.38.1(eslint@8.24.0)(typescript@4.8.4): - resolution: {integrity: sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/parser@6.14.0(eslint@8.24.0)(typescript@5.3.3): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 5.38.1 - '@typescript-eslint/types': 5.38.1 - '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4 eslint: 8.24.0 - typescript: 4.8.4 + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@5.38.1: - resolution: {integrity: sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/scope-manager@6.14.0: + resolution: {integrity: sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==} + engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 5.38.1 - '@typescript-eslint/visitor-keys': 5.38.1 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 dev: true - /@typescript-eslint/type-utils@5.38.1(eslint@8.24.0)(typescript@4.8.4): - resolution: {integrity: sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/type-utils@6.14.0(eslint@8.24.0)(typescript@5.3.3): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: '*' + eslint: ^7.0.0 || ^8.0.0 typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) - '@typescript-eslint/utils': 5.38.1(eslint@8.24.0)(typescript@4.8.4) + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.24.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.24.0 - tsutils: 3.21.0(typescript@4.8.4) - typescript: 4.8.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@5.38.1: - resolution: {integrity: sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/types@6.14.0: + resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} + engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@5.38.1(typescript@4.8.4): - resolution: {integrity: sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.3.3): + resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' peerDependenciesMeta: typescript: optional: true dependencies: - '@typescript-eslint/types': 5.38.1 - '@typescript-eslint/visitor-keys': 5.38.1 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 - tsutils: 3.21.0(typescript@4.8.4) - typescript: 4.8.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.38.1(eslint@8.24.0)(typescript@4.8.4): - resolution: {integrity: sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/utils@6.14.0(eslint@8.24.0)(typescript@5.3.3): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} + engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + eslint: ^7.0.0 || ^8.0.0 dependencies: - '@types/json-schema': 7.0.12 - '@typescript-eslint/scope-manager': 5.38.1 - '@typescript-eslint/types': 5.38.1 - '@typescript-eslint/typescript-estree': 5.38.1(typescript@4.8.4) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.24.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) eslint: 8.24.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.24.0) + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@5.38.1: - resolution: {integrity: sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@typescript-eslint/visitor-keys@6.14.0: + resolution: {integrity: sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==} + engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 5.38.1 + '@typescript-eslint/types': 6.14.0 eslint-visitor-keys: 3.4.3 dev: true @@ -6348,6 +6378,14 @@ packages: bindings: 1.5.0 prebuild-install: 7.1.1 + /better-sqlite3@9.2.2: + resolution: {integrity: sha512-qwjWB46il0lsDkeB4rSRI96HyDQr8sxeu1MkBVLMrwusq1KRu4Bpt1TMI+8zIJkDUtZ3umjAkaEjIlokZKWCQw==} + requiresBuild: true + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.1 + dev: false + /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} @@ -7498,14 +7536,6 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - /eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -7616,11 +7646,6 @@ packages: estraverse: 5.3.0 dev: true - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true - /estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -7807,7 +7832,6 @@ packages: resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==} dependencies: punycode: 1.4.1 - dev: false /fast-xml-parser@4.0.11: resolution: {integrity: sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==} @@ -8139,7 +8163,6 @@ packages: /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: false /handlebars@4.7.7: resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} @@ -8355,6 +8378,11 @@ packages: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + dev: true + /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -8765,7 +8793,7 @@ packages: pretty-format: 28.1.3 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@5.3.3) transitivePeerDependencies: - supports-color dev: true @@ -8805,7 +8833,7 @@ packages: pretty-format: 28.1.3 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4) + ts-node: 10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@5.3.3) transitivePeerDependencies: - supports-color dev: true @@ -9800,7 +9828,7 @@ packages: mimic-fn: 2.1.0 dev: true - /opentelemetry-plugin-better-sqlite3@1.1.0(better-sqlite3@7.6.2): + /opentelemetry-plugin-better-sqlite3@1.1.0(better-sqlite3@9.2.2): resolution: {integrity: sha512-yd+mgaB5W5JxzcQt9TvX1VIrusqtbbeuxSoZ6KQe4Ra0J/Kqkp6kz7dg0VQUU5+cenOWkza6xtvsT0KGXI03HA==} peerDependencies: better-sqlite3: ^7.1.1 || ^8.0.0 || ^9.0.0 @@ -9809,7 +9837,7 @@ packages: '@opentelemetry/core': 1.18.1(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.44.0(@opentelemetry/api@1.7.0) '@opentelemetry/semantic-conventions': 1.18.1 - better-sqlite3: 7.6.2 + better-sqlite3: 9.2.2 transitivePeerDependencies: - supports-color dev: false @@ -10053,7 +10081,6 @@ packages: pino: 8.15.0 pino-std-serializers: 6.2.2 process-warning: 2.2.0 - dev: false /pino-http@8.4.0: resolution: {integrity: sha512-9I1eRLxsujQJwLQTrHBU0wDlwnry2HzV2TlDwAsmZ9nT3Y2NQBLrz+DYp73L4i11vl/eudnFT8Eg0Kp62tMwEw==} @@ -10273,7 +10300,6 @@ packages: /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} - dev: false /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} @@ -11175,6 +11201,15 @@ packages: engines: {node: '>=8'} dev: true + /ts-api-utils@1.0.3(typescript@5.3.3): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.3.3 + dev: true + /ts-morph@16.0.0: resolution: {integrity: sha512-jGNF0GVpFj0orFw55LTsQxVYEUOCWBAbR5Ls7fTYE5pQsbW18ssTb/6UXx/GYAEjS+DQTp8VoTw0vqYMiaaQuw==} dependencies: @@ -11182,7 +11217,7 @@ packages: code-block-writer: 11.0.3 dev: false - /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@4.8.4): + /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.0.0)(typescript@5.3.3): resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==} hasBin: true peerDependencies: @@ -11209,12 +11244,12 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.8.4 + typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true - /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@18.17.8)(typescript@4.9.5): + /ts-node@10.8.2(@swc/core@1.3.42)(@types/node@20.10.4)(typescript@5.3.3): resolution: {integrity: sha512-LYdGnoGddf1D6v8REPtIH+5iq/gTDuZqv2/UJUU7tKjuEU8xVZorBM+buCGNjj+pGEud+sOoM4CX3/YzINpENA==} hasBin: true peerDependencies: @@ -11234,20 +11269,21 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 18.17.8 + '@types/node': 20.10.4 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.9.5 + typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 dev: true /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false /tslib@2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} @@ -11257,16 +11293,6 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} requiresBuild: true - /tsutils@3.21.0(typescript@4.8.4): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - engines: {node: '>= 6'} - peerDependencies: - typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - dependencies: - tslib: 1.14.1 - typescript: 4.8.4 - dev: true - /tty-table@4.2.1: resolution: {integrity: sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==} engines: {node: '>=8.0.0'} @@ -11383,15 +11409,9 @@ packages: hasBin: true dev: true - /typescript@4.8.4: - resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true - - /typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} + /typescript@5.3.3: + resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} + engines: {node: '>=14.17'} hasBin: true dev: true @@ -11416,6 +11436,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + /undici@5.28.2: resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} engines: {node: '>=14.0'}