Skip to content

Commit

Permalink
Appview: apply needs-review to notifications, threads, quote lists (#…
Browse files Browse the repository at this point in the history
…3058)

* appview: apply needs-review to notifications, threads, quote lists

* appview: tests for needs-review labels

* appview: apply needs-review to mentions
  • Loading branch information
devinivy authored Nov 21, 2024
1 parent 828f17f commit 21fd024
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 14 deletions.
6 changes: 1 addition & 5 deletions packages/bsky/src/api/app/bsky/feed/getPostThread.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Server } from '../../../../lexicon'
import {
isNotFoundPost,
isThreadViewPost,
} from '../../../../lexicon/types/app/bsky/feed/defs'
import { isRecord as isPostRecord } from '../../../../lexicon/types/app/bsky/feed/post'
import { isNotFoundPost } from '../../../../lexicon/types/app/bsky/feed/defs'
import {
QueryParams,
OutputSchema,
Expand Down
17 changes: 12 additions & 5 deletions packages/bsky/src/api/app/bsky/feed/getQuotes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AtUri } from '@atproto/syntax'
import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { createPipeline } from '../../../../pipeline'
Expand All @@ -15,7 +14,12 @@ import { parseString } from '../../../../hydration/util'
import { uriToDid } from '../../../../util/uris'

export default function (server: Server, ctx: AppContext) {
const getQuotes = createPipeline(skeleton, hydration, noBlocks, presentation)
const getQuotes = createPipeline(
skeleton,
hydration,
noBlocksOrNeedsReview,
presentation,
)
server.app.bsky.feed.getQuotes({
auth: ctx.authVerifier.standardOptional,
handler: async ({ params, auth, req }) => {
Expand Down Expand Up @@ -67,16 +71,19 @@ const hydration = async (inputs: {
)
}

const noBlocks = (inputs: {
const noBlocksOrNeedsReview = (inputs: {
ctx: Context
skeleton: Skeleton
hydration: HydrationState
}) => {
const { ctx, skeleton, hydration } = inputs
skeleton.uris = skeleton.uris.filter((uri) => {
const embedBlock = hydration.postBlocks?.get(uri)?.embed
const authorDid = uriToDid(uri)
return !ctx.views.viewerBlockExists(authorDid, hydration) && !embedBlock
return (
!ctx.views.viewerBlockExists(authorDid, hydration) &&
!hydration.postBlocks?.get(uri)?.embed &&
ctx.views.viewerSeesNeedsReview(authorDid, hydration)
)
})
return skeleton
}
Expand Down
18 changes: 14 additions & 4 deletions packages/bsky/src/api/app/bsky/notification/listNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function (server: Server, ctx: AppContext) {
const listNotifications = createPipeline(
skeleton,
hydration,
noBlockOrMutes,
noBlockOrMutesOrNeedsReview,
presentation,
)
server.app.bsky.notification.listNotifications({
Expand Down Expand Up @@ -88,7 +88,7 @@ const hydration = async (
return ctx.hydrator.hydrateNotifications(skeleton.notifs, params.hydrateCtx)
}

const noBlockOrMutes = (
const noBlockOrMutesOrNeedsReview = (
input: RulesFnInput<Context, Params, SkeletonState>,
) => {
const { skeleton, hydration, ctx, params } = input
Expand All @@ -110,18 +110,28 @@ const noBlockOrMutes = (
: undefined
const isRootPostByViewer =
rootPostUri && didFromUri(rootPostUri) === params.hydrateCtx?.viewer
const isHiddenReply = isRootPostByViewer
const isHiddenByThreadgate = isRootPostByViewer
? ctx.views.replyIsHiddenByThreadgate(
item.uri,
rootPostUri,
hydration,
)
: false
if (isHiddenReply) {
if (isHiddenByThreadgate) {
return false
}
}
}
// Filter out notifications from users that need review unless moots
if (
item.reason === 'reply' ||
item.reason === 'quote' ||
item.reason === 'mention'
) {
if (!ctx.views.viewerSeesNeedsReview(did, hydration)) {
return false
}
}
return true
})
return skeleton
Expand Down
10 changes: 10 additions & 0 deletions packages/bsky/src/hydration/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type { Label } from '../lexicon/types/com/atproto/label/defs'

export type SubjectLabels = {
isTakendown: boolean
needsReview: boolean
labels: HydrationMap<Label> // src + val -> label
}

Expand Down Expand Up @@ -84,6 +85,7 @@ export class LabelHydrator {
if (!entry) {
entry = {
isTakendown: false,
needsReview: false,
labels: new HydrationMap(),
}
acc.set(label.uri, entry)
Expand All @@ -96,6 +98,13 @@ export class LabelHydrator {
) {
entry.isTakendown = true
}
if (
label.val === NEEDS_REVIEW_LABEL &&
!label.neg &&
labelers.redact.has(label.src)
) {
entry.needsReview = true
}
return acc
}, new Labels())
}
Expand Down Expand Up @@ -147,3 +156,4 @@ const labelerDidToUri = (did: string): string => {
}

const TAKEDOWN_LABELS = ['!takedown', '!suspend']
const NEEDS_REVIEW_LABEL = 'needs-review'
12 changes: 12 additions & 0 deletions packages/bsky/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ export class Views {
return actor.muted || !!actor.mutedByList
}

viewerSeesNeedsReview(did: string, state: HydrationState): boolean {
const { labels, profileViewers, ctx } = state
return (
!labels?.get(did)?.needsReview ||
ctx?.viewer === did ||
!!profileViewers?.get(did)?.following
)
}

replyIsHiddenByThreadgate(
replyUri: string,
rootPostUri: string,
Expand Down Expand Up @@ -850,6 +859,9 @@ export class Views {
if (this.viewerBlockExists(post.author.did, state)) {
return this.blockedPost(uri, post.author.did, state)
}
if (!this.viewerSeesNeedsReview(post.author.did, state)) {
return undefined
}
return {
$type: 'app.bsky.feed.defs#threadViewPost',
post,
Expand Down
168 changes: 168 additions & 0 deletions packages/bsky/tests/views/labels-needs-review.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { AtpAgent } from '@atproto/api'
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
import { ids } from '../../src/lexicon/lexicons'
import assert from 'assert'
import { isThreadViewPost } from '../../src/lexicon/types/app/bsky/feed/defs'

describe('bsky needs-review labels', () => {
let network: TestNetwork
let agent: AtpAgent
let sc: SeedClient

beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_views_needs_review_labels',
})
agent = network.bsky.getClient()
sc = network.getSeedClient()
await basicSeed(sc)

await sc.createAccount('geoff', {
email: '[email protected]',
handle: 'geoff.test',
password: 'geoff',
})

await sc.reply(
sc.dids.geoff,
sc.posts[sc.dids.alice][0].ref,
sc.posts[sc.dids.alice][0].ref,
'my name geoff',
)

await sc.post(
sc.dids.geoff,
'her name alice',
undefined,
undefined,
sc.posts[sc.dids.alice][0].ref,
)

await sc.follow(sc.dids.bob, sc.dids.geoff)

await network.processAll()

AtpAgent.configure({ appLabelers: [network.ozone.ctx.cfg.service.did] })
await network.bsky.db.db
.insertInto('label')
.values({
src: network.ozone.ctx.cfg.service.did,
uri: sc.dids.geoff,
cid: '',
val: 'needs-review',
neg: false,
cts: new Date().toISOString(),
})
.execute()
})

afterAll(async () => {
await network.close()
})

it('applies to thread replies.', async () => {
const {
data: { thread },
} = await agent.app.bsky.feed.getPostThread({
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
})
assert(isThreadViewPost(thread))
expect(
thread.replies?.some((reply) => {
return (
isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
)
}),
).toBe(false)
})

it('applies to quote lists.', async () => {
const {
data: { posts },
} = await agent.app.bsky.feed.getQuotes({
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
})
expect(
posts.some((post) => {
return post.author.did === sc.dids.geoff
}),
).toBe(false)
})

it('applies to reply, quote, and mention notifications.', async () => {
const {
data: { notifications },
} = await agent.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
sc.dids.alice,
ids.AppBskyNotificationListNotifications,
),
},
)
expect(
notifications.some((notif) => {
return notif.reason === 'reply' && notif.author.did === sc.dids.geoff
}),
).toBe(false)
expect(
notifications.some((notif) => {
return notif.reason === 'quote' && notif.author.did === sc.dids.geoff
}),
).toBe(false)
expect(
notifications.some((notif) => {
return notif.reason === 'mention' && notif.author.did === sc.dids.geoff
}),
).toBe(false)
})

it('does not apply to self.', async () => {
const {
data: { thread },
} = await agent.app.bsky.feed.getPostThread(
{
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
},
{
headers: await network.serviceHeaders(
sc.dids.geoff,
ids.AppBskyFeedGetPostThread,
),
},
)
assert(isThreadViewPost(thread))
expect(
thread.replies?.some((reply) => {
return (
isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
)
}),
).toBe(true)
})

it('does not apply to followers.', async () => {
const {
data: { thread },
} = await agent.app.bsky.feed.getPostThread(
{
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
},
{
headers: await network.serviceHeaders(
sc.dids.bob, // follows geoff
ids.AppBskyFeedGetPostThread,
),
},
)
assert(isThreadViewPost(thread))
expect(
thread.replies?.some((reply) => {
return (
isThreadViewPost(reply) && reply.post.author.did === sc.dids.geoff
)
}),
).toBe(true)
})
})

0 comments on commit 21fd024

Please sign in to comment.