Skip to content

Commit

Permalink
Put canReply state on post viewer state instead of thread viewer state (
Browse files Browse the repository at this point in the history
#1882)

* switch canReply from thread to post view

* tweaks & fix up tests

* update snaps

* fix more snaps

* hydrate feed items for getPosts & searchPosts

* fix another snapshot

* getPosts test

* canReply -> blockedByGate & DRY up code

* blockedByGate -> excludedByGate

* replyDisabled
  • Loading branch information
dholms authored Nov 28, 2023
1 parent 95d33f7 commit 7edad62
Show file tree
Hide file tree
Showing 31 changed files with 261 additions and 322 deletions.
12 changes: 3 additions & 9 deletions lexicons/app/bsky/feed/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"type": "object",
"properties": {
"repost": { "type": "string", "format": "at-uri" },
"like": { "type": "string", "format": "at-uri" }
"like": { "type": "string", "format": "at-uri" },
"replyDisabled": { "type": "boolean" }
}
},
"feedViewPost": {
Expand Down Expand Up @@ -87,8 +88,7 @@
"type": "union",
"refs": ["#threadViewPost", "#notFoundPost", "#blockedPost"]
}
},
"viewer": { "type": "ref", "ref": "#viewerThreadState" }
}
}
},
"notFoundPost": {
Expand Down Expand Up @@ -116,12 +116,6 @@
"viewer": { "type": "ref", "ref": "app.bsky.actor.defs#viewerState" }
}
},
"viewerThreadState": {
"type": "object",
"properties": {
"canReply": { "type": "boolean" }
}
},
"generatorView": {
"type": "object",
"required": ["uri", "cid", "did", "creator", "displayName", "indexedAt"],
Expand Down
15 changes: 3 additions & 12 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4892,6 +4892,9 @@ export const schemaDict = {
type: 'string',
format: 'at-uri',
},
replyDisabled: {
type: 'boolean',
},
},
},
feedViewPost: {
Expand Down Expand Up @@ -4975,10 +4978,6 @@ export const schemaDict = {
],
},
},
viewer: {
type: 'ref',
ref: 'lex:app.bsky.feed.defs#viewerThreadState',
},
},
},
notFoundPost: {
Expand Down Expand Up @@ -5027,14 +5026,6 @@ export const schemaDict = {
},
},
},
viewerThreadState: {
type: 'object',
properties: {
canReply: {
type: 'boolean',
},
},
},
generatorView: {
type: 'object',
required: ['uri', 'cid', 'did', 'creator', 'displayName', 'indexedAt'],
Expand Down
19 changes: 1 addition & 18 deletions packages/api/src/client/types/app/bsky/feed/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function validatePostView(v: unknown): ValidationResult {
export interface ViewerState {
repost?: string
like?: string
replyDisabled?: boolean
[k: string]: unknown
}

Expand Down Expand Up @@ -137,7 +138,6 @@ export interface ThreadViewPost {
| BlockedPost
| { $type: string; [k: string]: unknown }
)[]
viewer?: ViewerThreadState
[k: string]: unknown
}

Expand Down Expand Up @@ -208,23 +208,6 @@ export function validateBlockedAuthor(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#blockedAuthor', v)
}

export interface ViewerThreadState {
canReply?: boolean
[k: string]: unknown
}

export function isViewerThreadState(v: unknown): v is ViewerThreadState {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'app.bsky.feed.defs#viewerThreadState'
)
}

export function validateViewerThreadState(v: unknown): ValidationResult {
return lexicons.validate('app.bsky.feed.defs#viewerThreadState', v)
}

export interface GeneratorView {
uri: string
cid: string
Expand Down
4 changes: 1 addition & 3 deletions packages/bsky/src/api/app/bsky/feed/getActorLikes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ const noPostBlocks = (state: HydrationState) => {
const presentation = (state: HydrationState, ctx: Context) => {
const { feedService } = ctx
const { feedItems, cursor, params } = state
const feed = feedService.views.formatFeed(feedItems, state, {
viewer: params.viewer,
})
const feed = feedService.views.formatFeed(feedItems, state, params.viewer)
return { feed, cursor }
}

Expand Down
4 changes: 1 addition & 3 deletions packages/bsky/src/api/app/bsky/feed/getAuthorFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,7 @@ const noBlocksOrMutedReposts = (state: HydrationState) => {
const presentation = (state: HydrationState, ctx: Context) => {
const { feedService } = ctx
const { feedItems, cursor, params } = state
const feed = feedService.views.formatFeed(feedItems, state, {
viewer: params.viewer,
})
const feed = feedService.views.formatFeed(feedItems, state, params.viewer)
return { feed, cursor }
}

Expand Down
4 changes: 1 addition & 3 deletions packages/bsky/src/api/app/bsky/feed/getFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,7 @@ const noBlocksOrMutes = (state: HydrationState) => {
const presentation = (state: HydrationState, ctx: Context) => {
const { feedService } = ctx
const { feedItems, cursor, passthrough, params } = state
const feed = feedService.views.formatFeed(feedItems, state, {
viewer: params.viewer,
})
const feed = feedService.views.formatFeed(feedItems, state, params.viewer)
return {
feed,
cursor,
Expand Down
4 changes: 1 addition & 3 deletions packages/bsky/src/api/app/bsky/feed/getListFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,7 @@ const noBlocksOrMutes = (state: HydrationState) => {
const presentation = (state: HydrationState, ctx: Context) => {
const { feedService } = ctx
const { feedItems, cursor, params } = state
const feed = feedService.views.formatFeed(feedItems, state, {
viewer: params.viewer,
})
const feed = feedService.views.formatFeed(feedItems, state, params.viewer)
return { feed, cursor }
}

Expand Down
69 changes: 15 additions & 54 deletions packages/bsky/src/api/app/bsky/feed/getPostThread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,21 @@ import {
NotFoundPost,
ThreadViewPost,
isNotFoundPost,
isThreadViewPost,
} from '../../../../lexicon/types/app/bsky/feed/defs'
import { Record as PostRecord } from '../../../../lexicon/types/app/bsky/feed/post'
import { Record as ThreadgateRecord } from '../../../../lexicon/types/app/bsky/feed/threadgate'
import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPostThread'
import AppContext from '../../../../context'
import {
FeedService,
FeedRow,
FeedHydrationState,
PostInfo,
} from '../../../../services/feed'
import {
getAncestorsAndSelfQb,
getDescendentsQb,
} from '../../../../services/util/post'
import { Database } from '../../../../db'
import DatabaseSchema from '../../../../db/database-schema'
import { setRepoRev } from '../../../util'
import { ActorInfoMap, ActorService } from '../../../../services/actor'
import { violatesThreadGate } from '../../../../services/feed/util'
import { createPipeline, noRules } from '../../../../pipeline'

export default function (server: Server, ctx: AppContext) {
Expand Down Expand Up @@ -80,21 +74,7 @@ const hydration = async (state: SkeletonState, ctx: Context) => {
} = state
const relevant = getRelevantIds(threadData)
const hydrated = await feedService.feedHydration({ ...relevant, viewer })
// check root reply interaction rules
const anchorPostUri = threadData.post.postUri
const rootUri = threadData.post.replyRoot || anchorPostUri
const anchor = hydrated.posts[anchorPostUri]
const root = hydrated.posts[rootUri]
const gate = hydrated.threadgates[rootUri]?.record
const viewerCanReply = await checkViewerCanReply(
ctx.db.db,
anchor ?? null,
viewer,
new AtUri(rootUri).host,
(root?.record ?? null) as PostRecord | null,
gate ?? null,
)
return { ...state, ...hydrated, viewerCanReply }
return { ...state, ...hydrated }
}

const presentation = (state: HydrationState, ctx: Context) => {
Expand All @@ -103,16 +83,19 @@ const presentation = (state: HydrationState, ctx: Context) => {
const actors = actorService.views.profileBasicPresentation(
Object.keys(profiles),
state,
{ viewer: params.viewer },
params.viewer,
)
const thread = composeThread(
state.threadData,
actors,
state,
ctx,
params.viewer,
)
const thread = composeThread(state.threadData, actors, state, ctx)
if (isNotFoundPost(thread)) {
// @TODO technically this could be returned as a NotFoundPost based on lexicon
throw new InvalidRequestError(`Post not found: ${params.uri}`, 'NotFound')
}
if (isThreadViewPost(thread) && params.viewer) {
thread.viewer = { canReply: state.viewerCanReply }
}
return { thread }
}

Expand All @@ -121,6 +104,7 @@ const composeThread = (
actors: ActorInfoMap,
state: HydrationState,
ctx: Context,
viewer: string | null,
) => {
const { feedService } = ctx
const { posts, threadgates, embeds, blocks, labels, lists } = state
Expand All @@ -133,6 +117,7 @@ const composeThread = (
embeds,
labels,
lists,
viewer,
)

// replies that are invalid due to reply-gating:
Expand Down Expand Up @@ -179,14 +164,14 @@ const composeThread = (
notFound: true,
}
} else {
parent = composeThread(threadData.parent, actors, state, ctx)
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)
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
Expand Down Expand Up @@ -223,6 +208,7 @@ const getRelevantIds = (
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 }
}
Expand Down Expand Up @@ -317,28 +303,6 @@ const getChildrenData = (
}))
}

const checkViewerCanReply = async (
db: DatabaseSchema,
anchor: PostInfo | null,
viewer: string | null,
owner: string,
root: PostRecord | null,
threadgate: ThreadgateRecord | null,
) => {
if (!viewer) return false
// @TODO re-enable invalidReplyRoot check
// if (anchor?.invalidReplyRoot || anchor?.violatesThreadGate) return false
if (anchor?.violatesThreadGate) return false
const viewerViolatesThreadGate = await violatesThreadGate(
db,
viewer,
owner,
root,
threadgate,
)
return !viewerViolatesThreadGate
}

class ParentNotFoundError extends Error {
constructor(public uri: string) {
super(`Parent not found: ${uri}`)
Expand All @@ -364,7 +328,4 @@ type SkeletonState = {
threadData: PostThread
}

type HydrationState = SkeletonState &
FeedHydrationState & {
viewerCanReply: boolean
}
type HydrationState = SkeletonState & FeedHydrationState
39 changes: 21 additions & 18 deletions packages/bsky/src/api/app/bsky/feed/getPosts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { dedupeStrs } from '@atproto/common'
import { AtUri } from '@atproto/syntax'
import { Server } from '../../../../lexicon'
import { QueryParams } from '../../../../lexicon/types/app/bsky/feed/getPosts'
import AppContext from '../../../../context'
import { Database } from '../../../../db'
import { FeedHydrationState, FeedService } from '../../../../services/feed'
import {
FeedHydrationState,
FeedRow,
FeedService,
} from '../../../../services/feed'
import { createPipeline } from '../../../../pipeline'
import { ActorService } from '../../../../services/actor'

Expand All @@ -31,51 +34,51 @@ export default function (server: Server, ctx: AppContext) {
})
}

const skeleton = async (params: Params) => {
return { params, postUris: dedupeStrs(params.uris) }
const skeleton = async (params: Params, ctx: Context) => {
const deduped = dedupeStrs(params.uris)
const feedItems = await ctx.feedService.postUrisToFeedItems(deduped)
return { params, feedItems }
}

const hydration = async (state: SkeletonState, ctx: Context) => {
const { feedService } = ctx
const { params, postUris } = state
const uris = new Set<string>(postUris)
const dids = new Set<string>(postUris.map((uri) => new AtUri(uri).hostname))
const { params, feedItems } = state
const refs = feedService.feedItemRefs(feedItems)
const hydrated = await feedService.feedHydration({
uris,
dids,
...refs,
viewer: params.viewer,
})
return { ...state, ...hydrated }
}

const noBlocks = (state: HydrationState) => {
const { viewer } = state.params
state.postUris = state.postUris.filter((uri) => {
const post = state.posts[uri]
if (!viewer || !post) return true
return !state.bam.block([viewer, post.creator])
state.feedItems = state.feedItems.filter((item) => {
if (!viewer) return true
return !state.bam.block([viewer, item.postAuthorDid])
})
return state
}

const presentation = (state: HydrationState, ctx: Context) => {
const { feedService, actorService } = ctx
const { postUris, profiles, params } = state
const { feedItems, profiles, params } = state
const SKIP = []
const actors = actorService.views.profileBasicPresentation(
Object.keys(profiles),
state,
{ viewer: params.viewer },
params.viewer,
)
const postViews = postUris.flatMap((uri) => {
const postViews = feedItems.flatMap((item) => {
const postView = feedService.views.formatPostView(
uri,
item.postUri,
actors,
state.posts,
state.threadgates,
state.embeds,
state.labels,
state.lists,
params.viewer,
)
return postView ?? SKIP
})
Expand All @@ -92,7 +95,7 @@ type Params = QueryParams & { viewer: string | null }

type SkeletonState = {
params: Params
postUris: string[]
feedItems: FeedRow[]
}

type HydrationState = SkeletonState & FeedHydrationState
Loading

0 comments on commit 7edad62

Please sign in to comment.