atproto/packages/bsky/tests/views/feed-hidden-replies.test.ts
Eric Bailey aba664fbdf
Detached QPs and hidden replies (#2675)
* Add new postgate lex, hiddeReplies to threadgate, codegen

* Add protobufs

* Add to mock dataplane

* Add matching postgate method to feed hydration methods

* Add to getRecord

* Add to HydrationState

* Fix typo

* Add to mergeStates, fetch embeds in threads

* Integrate into embed views

* Add test for QPs in threads

* Add feed test

* Fix naming convention in protos

* Add #viewRemoved record view, rename postgate.json

* Integrate new view

* Filter hidden replies from feeds

* Filter out replies at the handler level, do not filter for author feeds

* Fix lint

* Move hidden reply check to view layer

* Reduce, reuse, recycle

* Rename to lowercase

* Rename layer vars

* Add quote gate props to postgate (#2693)

* Add quote gate props to postgate

* Consistent naming

* Fix record structure

* Codegen

* Show hidden replies in author feed

* Allow reposts of hidden replies

* Lex and codegen

* Add violates_quote_gate to proto

* Consistent naming, codegen

* Integrate violatesQuotegate and canQuotepost

* Remove rules, codegen

* Hydrate all postgates for all requested posts

* Match other impl

* Add test, need to split these out

* Format

* Hydrate first nested embeds too

* Add postgate test suite

* Add violatesQuoteGate to dataplane

* Ingest and set violatesQuoteGate, return on meta

* Return removed embed for quotes that violate gate

* Add test

* Dedupe URIs before fetching postgates

* Update snaps

* Snap

* Format

* Updating naming conventions for postgate-related attributes

* Correct naming

* Consistency

* Proto too

* Rename to viewDetached

* Codegen

* Rename everything

* Codegen

* Quotes that violate a quote gate can still be quoted themselves

* Couple more renames

* Snaps

* Ensure reply ref is tombstoned for hidden replies

* Split out hidden replies tests and create fresh fixture

* Hydrate threadgates for reply notifications, filter hidden replies

* Remove snap

* Add flaky test

* Rename violatesEmbeddingRules

* Fix flaky test

* Only write to db if violatesEmbeddingRules is true

* DRY up post uri -> gate uri logic

* isThreadgateListRule

* Don't share users object between tests

* No pascal

* Remove default params

* Find -> some

* canQuotepost -> canEmbed, remove optional

* Fix quoteee typo

* await follows

* Throw in post uri -> gate utils

* Ensure fetch threadgates for reply roots

* Don't hydrate threadgates twice

* DRY up uri -> did parsing

* Clean up parsePostgate logic

* Format

* Revert change

* Revert change

* Replace a couple more uri->did conversions

* Only filter replies from feeds if viewer hid them

* Revert, filter out replies that are hidden from feeds

* Remove old test

* Replace uri->did util

* Revert change to unused file

* Only validatePostEmbed and check postgates for post records

* Ensure notifications aren't generated down a hidden reply chain

* Changeset

* Cleanup

* Fix notification filtering logic

* Simplify

* Don't notify for invalid embeds

* Use new APIs

* Add hasPostGate and hasThreadGate flags from dataplane

* Only fetch postgates if post has one

* Only fetch threadgates if post has one or was deleted

* Remove notification filtering

* Don't hydrate threadgates for notifications

* Move hidden replies in feeds to match block handling

* Do no filtering of hidden replies in feeds

* Revert "Don't hydrate threadgates for notifications"

This reverts commit 1dcec0b239a7b9d6800427b26b8ba3e6a54210f9.

* Revert "Remove notification filtering"

This reverts commit 1e7069dfd809d1f18e9f05fd1d422e7399aa1bb0.

* Filter notifications for OP only

* Add additional check to hidden replies test

* Move noty filter logic into method handler

* Update .changeset/perfect-parrots-appear.md

Co-authored-by: devin ivy <devinivy@gmail.com>

* Update packages/bsky/tests/seed/postgates.ts

Co-authored-by: devin ivy <devinivy@gmail.com>

* Another structuredClone

* Update packages/bsky/src/hydration/hydrator.ts

Co-authored-by: devin ivy <devinivy@gmail.com>

* Better comment

* Update packages/bsky/src/data-plane/server/indexing/plugins/post.ts

Co-authored-by: devin ivy <devinivy@gmail.com>

* Regen protos to match dataplane

* Update quotes snap to include embeddingDisabled

* Clarify usage of post uri -> gate utils

---------

Co-authored-by: devin ivy <devinivy@gmail.com>
2024-08-21 14:36:51 -05:00

247 lines
7.1 KiB
TypeScript

import { TestNetwork, SeedClient } from '@atproto/dev-env'
import AtpAgent from '@atproto/api'
import { ids } from '../../src/lexicon/lexicons'
import { feedHiddenRepliesSeed, Users } from '../seed/feed-hidden-replies'
describe('feed hidden replies', () => {
let network: TestNetwork
let agent: AtpAgent
let pdsAgent: AtpAgent
let sc: SeedClient
let users: Users
beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'bsky_tests_feed_hidden_replies',
})
agent = network.bsky.getClient()
pdsAgent = network.pds.getClient()
sc = network.getSeedClient()
const result = await feedHiddenRepliesSeed(sc)
users = result.users
await network.processAll()
})
afterAll(async () => {
await network.close()
})
describe(`notifications`, () => {
it(`[A] -> [B] : B is hidden`, async () => {
const A = await sc.post(users.poster.did, `A`)
await network.processAll()
const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)
const C = await sc.reply(users.replier.did, A.ref, A.ref, `C`)
await pdsAgent.api.app.bsky.feed.threadgate.create(
{
repo: A.ref.uri.host,
rkey: A.ref.uri.rkey,
},
{
post: A.ref.uriStr,
createdAt: new Date().toISOString(),
hiddenReplies: [B.ref.uriStr],
},
sc.getHeaders(A.ref.uri.host),
)
await network.processAll()
const {
data: { notifications },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.poster.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const notificationB = notifications.find((item) => {
return item.uri === B.ref.uriStr
})
const notificationC = notifications.find((item) => {
return item.uri === C.ref.uriStr
})
expect(notificationB).toBeUndefined()
expect(notificationC).toBeDefined()
})
it(`[A] -> [B] -> [C] : B is hidden, C results in no notification for A, notification for B`, async () => {
const A = await sc.post(users.poster.did, `A`)
await network.processAll()
const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)
await pdsAgent.api.app.bsky.feed.threadgate.create(
{
repo: A.ref.uri.host,
rkey: A.ref.uri.rkey,
},
{
post: A.ref.uriStr,
createdAt: new Date().toISOString(),
hiddenReplies: [B.ref.uriStr],
},
sc.getHeaders(A.ref.uri.host),
)
await network.processAll()
const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`)
await network.processAll()
const {
data: { notifications: posterNotifications },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.poster.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const posterNotificationB = posterNotifications.find((item) => {
return item.uri === B.ref.uriStr
})
const posterNotificationC = posterNotifications.find((item) => {
return item.uri === C.ref.uriStr
})
expect(posterNotificationB).toBeUndefined()
expect(posterNotificationC).toBeUndefined()
const {
data: { notifications: replierNotifications },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.replier.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const replierNotificationC = replierNotifications.find((item) => {
return item.uri === C.ref.uriStr
})
expect(replierNotificationC).toBeDefined()
})
it(`[A] -> [B] -> [C] -> [D] : C is hidden, D results in no notification for A or B, notification for C, C exists in B's notifications`, async () => {
const A = await sc.post(users.poster.did, `A`)
await network.processAll()
const B = await sc.reply(users.replier.did, A.ref, A.ref, `B`)
await network.processAll()
const C = await sc.reply(users.viewer.did, A.ref, B.ref, `C`)
await network.processAll()
const {
data: { notifications: posterNotificationsBefore },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.poster.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const posterNotificationCBefore = posterNotificationsBefore.find(
(item) => {
return item.uri === C.ref.uriStr
},
)
expect(posterNotificationCBefore).toBeDefined()
await pdsAgent.api.app.bsky.feed.threadgate.create(
{
repo: A.ref.uri.host,
rkey: A.ref.uri.rkey,
},
{
post: A.ref.uriStr,
createdAt: new Date().toISOString(),
hiddenReplies: [C.ref.uriStr],
},
sc.getHeaders(A.ref.uri.host),
)
await network.processAll()
const D = await sc.reply(users.viewer.did, A.ref, C.ref, `D`)
await network.processAll()
const {
data: { notifications: posterNotifications },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.poster.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const posterNotificationB = posterNotifications.find((item) => {
return item.uri === B.ref.uriStr
})
const posterNotificationC = posterNotifications.find((item) => {
return item.uri === C.ref.uriStr
})
const posterNotificationD = posterNotifications.find((item) => {
return item.uri === D.ref.uriStr
})
expect(posterNotificationB).toBeDefined()
expect(posterNotificationC).toBeUndefined() // hidden bc OP
expect(posterNotificationD).toBeUndefined() // hidden bc no propogation
const {
data: { notifications: replierNotifications },
} = await agent.api.app.bsky.notification.listNotifications(
{},
{
headers: await network.serviceHeaders(
users.replier.did,
ids.AppBskyNotificationListNotifications,
),
},
)
const replierNotificationC = replierNotifications.find((item) => {
return item.uri === C.ref.uriStr
})
const replierNotificationD = replierNotifications.find((item) => {
return item.uri === D.ref.uriStr
})
expect(replierNotificationC).toBeDefined() // not hidden bc not OP
expect(replierNotificationD).toBeUndefined() // hidden bc no propogation
await pdsAgent.api.app.bsky.feed.threadgate.delete(
{
repo: A.ref.uri.host,
rkey: A.ref.uri.rkey,
},
sc.getHeaders(A.ref.uri.host),
)
await network.processAll()
})
})
})