a8e1f9000d
* Return `ThreadgateView` on response from `getPostThread` * Changeset * Format * Add to test * Clean up logic * Use suggestion from Dan
750 lines
24 KiB
TypeScript
750 lines
24 KiB
TypeScript
import assert from 'assert'
|
|
import { AtpAgent } from '@atproto/api'
|
|
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
|
|
import {
|
|
isNotFoundPost,
|
|
isThreadViewPost,
|
|
} from '../../src/lexicon/types/app/bsky/feed/defs'
|
|
import { forSnapshot } from '../_util'
|
|
import { ids } from '../../src/lexicon/lexicons'
|
|
|
|
describe('views with thread gating', () => {
|
|
let network: TestNetwork
|
|
let agent: AtpAgent
|
|
let pdsAgent: AtpAgent
|
|
let sc: SeedClient
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetwork.create({
|
|
dbPostgresSchema: 'bsky_views_thread_gating',
|
|
})
|
|
agent = network.bsky.getClient()
|
|
pdsAgent = network.pds.getClient()
|
|
sc = network.getSeedClient()
|
|
await basicSeed(sc)
|
|
await network.processAll()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
// check that replyDisabled state is applied correctly in a simple method like getPosts
|
|
const checkReplyDisabled = async (
|
|
uri: string,
|
|
user: string,
|
|
blocked: boolean | undefined,
|
|
) => {
|
|
const res = await agent.api.app.bsky.feed.getPosts(
|
|
{ uris: [uri] },
|
|
{ headers: await network.serviceHeaders(user, ids.AppBskyFeedGetPosts) },
|
|
)
|
|
expect(res.data.posts[0].viewer?.replyDisabled).toBe(blocked)
|
|
}
|
|
|
|
it('applies gate for empty rules.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'empty rules')
|
|
const { uri: threadgateUri } =
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{ post: post.ref.uriStr, createdAt: iso(), allow: [] },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
await sc.reply(sc.dids.alice, post.ref, post.ref, 'empty rules reply')
|
|
await network.processAll()
|
|
const {
|
|
data: { thread, threadgate },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(thread))
|
|
expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
|
|
expect(thread.post.viewer?.replyDisabled).toBe(true)
|
|
expect(thread.replies?.length).toEqual(0)
|
|
expect(threadgate?.uri).toEqual(threadgateUri)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
|
|
})
|
|
|
|
it('does not generate notifications when post violates threadgate.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'notifications')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{ post: post.ref.uriStr, createdAt: iso(), allow: [] },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
const reply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'notifications reply',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { notifications },
|
|
} = await agent.api.app.bsky.notification.listNotifications(
|
|
{},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.carol,
|
|
ids.AppBskyNotificationListNotifications,
|
|
),
|
|
},
|
|
)
|
|
const notificationFromReply = notifications.find(
|
|
(notif) => notif.uri === reply.ref.uriStr,
|
|
)
|
|
expect(notificationFromReply).toBeUndefined()
|
|
})
|
|
|
|
it('applies gate for mention rule.', async () => {
|
|
const post = await sc.post(
|
|
sc.dids.carol,
|
|
'mention rules @carol.test @dan.test',
|
|
[
|
|
{
|
|
index: { byteStart: 14, byteEnd: 25 },
|
|
features: [
|
|
{ $type: 'app.bsky.richtext.facet#mention', did: sc.dids.carol },
|
|
],
|
|
},
|
|
{
|
|
index: { byteStart: 26, byteEnd: 35 },
|
|
features: [
|
|
{ $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan },
|
|
],
|
|
},
|
|
],
|
|
)
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [{ $type: 'app.bsky.feed.threadgate#mentionRule' }],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'mention rule reply disallow',
|
|
)
|
|
const danReply = await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
post.ref,
|
|
'mention rule reply allow',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: aliceThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(aliceThread))
|
|
expect(aliceThread.post.viewer?.replyDisabled).toBe(true)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
|
|
const {
|
|
data: { thread: danThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(danThread))
|
|
expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
|
|
expect(danThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
|
|
const [reply, ...otherReplies] = danThread.replies ?? []
|
|
assert(isThreadViewPost(reply))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect(reply.post.uri).toEqual(danReply.ref.uriStr)
|
|
})
|
|
|
|
it('applies gate for following rule.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'following rule')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
// carol only follows alice
|
|
await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
post.ref,
|
|
'following rule reply disallow',
|
|
)
|
|
const aliceReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'following rule reply allow',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: danThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(danThread))
|
|
expect(danThread.post.viewer?.replyDisabled).toBe(true)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, true)
|
|
const {
|
|
data: { thread: aliceThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(aliceThread))
|
|
expect(forSnapshot(aliceThread.post.threadgate)).toMatchSnapshot()
|
|
expect(aliceThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
|
|
const [reply, ...otherReplies] = aliceThread.replies ?? []
|
|
assert(isThreadViewPost(reply))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)
|
|
})
|
|
|
|
it('applies gate for list rule.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'following rule')
|
|
// setup lists to allow alice and dan
|
|
const listA = await pdsAgent.api.app.bsky.graph.list.create(
|
|
{ repo: sc.dids.carol },
|
|
{
|
|
name: 'list a',
|
|
purpose: 'app.bsky.graph.defs#modlist',
|
|
createdAt: iso(),
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await pdsAgent.api.app.bsky.graph.listitem.create(
|
|
{ repo: sc.dids.carol },
|
|
{
|
|
list: listA.uri,
|
|
subject: sc.dids.alice,
|
|
createdAt: iso(),
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
const listB = await pdsAgent.api.app.bsky.graph.list.create(
|
|
{ repo: sc.dids.carol },
|
|
{
|
|
name: 'list b',
|
|
purpose: 'app.bsky.graph.defs#modlist',
|
|
createdAt: iso(),
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await pdsAgent.api.app.bsky.graph.listitem.create(
|
|
{ repo: sc.dids.carol },
|
|
{
|
|
list: listB.uri,
|
|
subject: sc.dids.dan,
|
|
createdAt: iso(),
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [
|
|
{ $type: 'app.bsky.feed.threadgate#listRule', list: listA.uri },
|
|
{ $type: 'app.bsky.feed.threadgate#listRule', list: listB.uri },
|
|
],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
//
|
|
await sc.reply(sc.dids.bob, post.ref, post.ref, 'list rule reply disallow')
|
|
const aliceReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'list rule reply allow (list a)',
|
|
)
|
|
const danReply = await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
post.ref,
|
|
'list rule reply allow (list b)',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: bobThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.bob,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(bobThread))
|
|
expect(bobThread.post.viewer?.replyDisabled).toBe(true)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, true)
|
|
const {
|
|
data: { thread: aliceThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(aliceThread))
|
|
expect(aliceThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
|
|
const {
|
|
data: { thread: danThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(danThread))
|
|
expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
|
|
expect(danThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
|
|
const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? []
|
|
assert(isThreadViewPost(reply1))
|
|
assert(isThreadViewPost(reply2))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect([reply1.post.uri, reply2.post.uri].sort()).toEqual(
|
|
[danReply.ref.uriStr, aliceReply.ref.uriStr].sort(),
|
|
)
|
|
})
|
|
|
|
it('applies gate for unknown list rule.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'unknown list rules')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [
|
|
{
|
|
$type: 'app.bsky.feed.threadgate#listRule',
|
|
list: post.ref.uriStr, // bad list link, references a post
|
|
},
|
|
],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'unknown list rules reply',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(thread))
|
|
expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
|
|
expect(thread.post.viewer?.replyDisabled).toBe(true)
|
|
expect(thread.replies?.length).toEqual(0)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, true)
|
|
})
|
|
|
|
it('applies gate for multiple rules.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'multi rules @dan.test', [
|
|
{
|
|
index: { byteStart: 12, byteEnd: 21 },
|
|
features: [
|
|
{ $type: 'app.bsky.richtext.facet#mention', did: sc.dids.dan },
|
|
],
|
|
},
|
|
])
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [
|
|
{ $type: 'app.bsky.feed.threadgate#mentionRule' },
|
|
{ $type: 'app.bsky.feed.threadgate#followingRule' },
|
|
],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
// carol only follows alice, and the post mentions dan.
|
|
await sc.reply(sc.dids.bob, post.ref, post.ref, 'multi rule reply disallow')
|
|
const aliceReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'multi rule reply allow (following)',
|
|
)
|
|
const danReply = await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
post.ref,
|
|
'multi rule reply allow (mention)',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: bobThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.bob,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(bobThread))
|
|
expect(bobThread.post.viewer?.replyDisabled).toBe(true)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.bob, true)
|
|
const {
|
|
data: { thread: aliceThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(aliceThread))
|
|
expect(aliceThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
|
|
const {
|
|
data: { thread: danThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(danThread))
|
|
expect(forSnapshot(danThread.post.threadgate)).toMatchSnapshot()
|
|
expect(danThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.dan, false)
|
|
const [reply1, reply2, ...otherReplies] = aliceThread.replies ?? []
|
|
assert(isThreadViewPost(reply1))
|
|
assert(isThreadViewPost(reply2))
|
|
expect(otherReplies.length).toEqual(0)
|
|
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 () => {
|
|
const post = await sc.post(sc.dids.carol, 'missing rules')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{ post: post.ref.uriStr, createdAt: iso() },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
const aliceReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'missing rules reply',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(thread))
|
|
expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
|
|
expect(thread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.alice, false)
|
|
const [reply, ...otherReplies] = thread.replies ?? []
|
|
assert(isThreadViewPost(reply))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)
|
|
})
|
|
|
|
it('applies gate after root post is deleted.', async () => {
|
|
// @NOTE also covers rule application more than one level deep
|
|
const post = await sc.post(sc.dids.carol, 'following rule w/ post deletion')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
// carol only follows alice
|
|
const orphanedReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
post.ref,
|
|
'following rule reply allow',
|
|
)
|
|
await pdsAgent.api.app.bsky.feed.post.delete(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
orphanedReply.ref,
|
|
'following rule reply disallow',
|
|
)
|
|
const aliceReply = await sc.reply(
|
|
sc.dids.alice,
|
|
post.ref,
|
|
orphanedReply.ref,
|
|
'following rule reply allow',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: danThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: orphanedReply.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(danThread))
|
|
expect(danThread.post.viewer?.replyDisabled).toBe(true)
|
|
await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.dan, true)
|
|
const {
|
|
data: { thread: aliceThread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: orphanedReply.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(aliceThread))
|
|
assert(
|
|
isNotFoundPost(aliceThread.parent) &&
|
|
aliceThread.parent.uri === post.ref.uriStr,
|
|
)
|
|
expect(aliceThread.post.threadgate).toMatchSnapshot()
|
|
expect(aliceThread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(orphanedReply.ref.uriStr, sc.dids.alice, false)
|
|
const [reply, ...otherReplies] = aliceThread.replies ?? []
|
|
assert(isThreadViewPost(reply))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect(reply.post.uri).toEqual(aliceReply.ref.uriStr)
|
|
})
|
|
|
|
it('does not apply gate to original poster.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'empty rules')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{ post: post.ref.uriStr, createdAt: iso(), allow: [] },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
const selfReply = await sc.reply(
|
|
sc.dids.carol,
|
|
post.ref,
|
|
post.ref,
|
|
'empty rules reply allow',
|
|
)
|
|
await network.processAll()
|
|
const {
|
|
data: { thread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: post.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.carol,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(thread))
|
|
expect(forSnapshot(thread.post.threadgate)).toMatchSnapshot()
|
|
expect(thread.post.viewer?.replyDisabled).toBe(false)
|
|
await checkReplyDisabled(post.ref.uriStr, sc.dids.carol, false)
|
|
const [reply, ...otherReplies] = thread.replies ?? []
|
|
assert(isThreadViewPost(reply))
|
|
expect(otherReplies.length).toEqual(0)
|
|
expect(reply.post.uri).toEqual(selfReply.ref.uriStr)
|
|
})
|
|
|
|
it('displays gated posts in feed and thread anchor without reply context.', async () => {
|
|
const post = await sc.post(sc.dids.carol, 'following rule')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: post.ref.uri.rkey },
|
|
{
|
|
post: post.ref.uriStr,
|
|
createdAt: iso(),
|
|
allow: [{ $type: 'app.bsky.feed.threadgate#followingRule' }],
|
|
},
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
// carol only follows alice
|
|
const badReply = await sc.reply(
|
|
sc.dids.dan,
|
|
post.ref,
|
|
post.ref,
|
|
'following rule reply disallow',
|
|
)
|
|
// going to ensure this one doesn't appear in badReply's thread
|
|
await sc.reply(sc.dids.alice, post.ref, badReply.ref, 'reply to disallowed')
|
|
await network.processAll()
|
|
// check thread view
|
|
const {
|
|
data: { thread },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: badReply.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(thread))
|
|
expect(thread.post.viewer?.replyDisabled).toBe(true) // nobody can reply to this, not even alice.
|
|
expect(thread.replies).toBeUndefined()
|
|
expect(thread.parent).toBeUndefined()
|
|
expect(thread.post.threadgate).toBeUndefined()
|
|
await checkReplyDisabled(badReply.ref.uriStr, sc.dids.alice, true)
|
|
// check feed view
|
|
const {
|
|
data: { feed },
|
|
} = await agent.api.app.bsky.feed.getAuthorFeed(
|
|
{ actor: sc.dids.dan },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetAuthorFeed,
|
|
),
|
|
},
|
|
)
|
|
const [feedItem] = feed
|
|
expect(feedItem.post.uri).toEqual(badReply.ref.uriStr)
|
|
expect(feedItem.post.threadgate).toBeUndefined()
|
|
expect(feedItem.reply).toBeUndefined()
|
|
})
|
|
|
|
it('does not apply gate unless it matches post rkey.', async () => {
|
|
const postA = await sc.post(sc.dids.carol, 'ungated a')
|
|
const postB = await sc.post(sc.dids.carol, 'ungated b')
|
|
await pdsAgent.api.app.bsky.feed.threadgate.create(
|
|
{ repo: sc.dids.carol, rkey: postA.ref.uri.rkey },
|
|
{ post: postB.ref.uriStr, createdAt: iso(), allow: [] },
|
|
sc.getHeaders(sc.dids.carol),
|
|
)
|
|
await network.processAll()
|
|
await sc.reply(sc.dids.alice, postA.ref, postA.ref, 'ungated reply')
|
|
await sc.reply(sc.dids.alice, postB.ref, postB.ref, 'ungated reply')
|
|
await network.processAll()
|
|
const {
|
|
data: { thread: threadA },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: postA.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(threadA))
|
|
expect(threadA.post.threadgate).toBeUndefined()
|
|
expect(threadA.post.viewer?.replyDisabled).toBeUndefined()
|
|
expect(threadA.replies?.length).toEqual(1)
|
|
await checkReplyDisabled(postA.ref.uriStr, sc.dids.alice, undefined)
|
|
const {
|
|
data: { thread: threadB },
|
|
} = await agent.api.app.bsky.feed.getPostThread(
|
|
{ uri: postB.ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
sc.dids.alice,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
assert(isThreadViewPost(threadB))
|
|
expect(threadB.post.threadgate).toBeUndefined()
|
|
expect(threadB.post.viewer?.replyDisabled).toBe(undefined)
|
|
await checkReplyDisabled(postB.ref.uriStr, sc.dids.alice, undefined)
|
|
expect(threadB.replies?.length).toEqual(1)
|
|
})
|
|
})
|
|
|
|
const iso = (date = new Date()) => date.toISOString()
|