638f5a8312
* Fix avatar path resolution in dev-env * changeset * extract dev-env assets to dedicated folder * add comment * fix fmt
499 lines
14 KiB
TypeScript
499 lines
14 KiB
TypeScript
import { AtpAgent, AtUri } from '@atproto/api'
|
|
import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env'
|
|
import { forSnapshot } from '../_util'
|
|
import { ids } from '../../src/lexicon/lexicons'
|
|
|
|
describe('bsky views with mutes from mute lists', () => {
|
|
let network: TestNetwork
|
|
let agent: AtpAgent
|
|
let pdsAgent: AtpAgent
|
|
let sc: SeedClient
|
|
|
|
let alice: string
|
|
let bob: string
|
|
let carol: string
|
|
let dan: string
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetwork.create({
|
|
dbPostgresSchema: 'bsky_views_mute_lists',
|
|
})
|
|
agent = network.bsky.getClient()
|
|
pdsAgent = network.pds.getClient()
|
|
sc = network.getSeedClient()
|
|
await basicSeed(sc)
|
|
alice = sc.dids.alice
|
|
bob = sc.dids.bob
|
|
carol = sc.dids.carol
|
|
dan = sc.dids.dan
|
|
// add follows to ensure mutes work even w follows
|
|
await sc.follow(carol, dan)
|
|
await sc.follow(dan, carol)
|
|
await network.processAll()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
let listUri: string
|
|
let listCid: string
|
|
|
|
it('creates a list with some items', async () => {
|
|
const avatar = await sc.uploadFile(
|
|
alice,
|
|
'../dev-env/assets/key-portrait-small.jpg',
|
|
'image/jpeg',
|
|
)
|
|
// alice creates mute list with bob & carol that dan uses
|
|
const list = await pdsAgent.api.app.bsky.graph.list.create(
|
|
{ repo: alice },
|
|
{
|
|
name: 'alice mutes',
|
|
purpose: 'app.bsky.graph.defs#modlist',
|
|
description: 'big list of mutes',
|
|
avatar: avatar.image,
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
listUri = list.uri
|
|
listCid = list.cid
|
|
await pdsAgent.api.app.bsky.graph.listitem.create(
|
|
{ repo: alice },
|
|
{
|
|
subject: sc.dids.bob,
|
|
list: list.uri,
|
|
reason: 'because',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
await pdsAgent.api.app.bsky.graph.listitem.create(
|
|
{ repo: alice },
|
|
{
|
|
subject: sc.dids.carol,
|
|
list: list.uri,
|
|
reason: 'idk',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
await pdsAgent.api.app.bsky.graph.listitem.create(
|
|
{ repo: alice },
|
|
{
|
|
subject: sc.dids.dan,
|
|
list: list.uri,
|
|
reason: 'idk',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
await network.processAll()
|
|
})
|
|
|
|
it('uses a list for mutes', async () => {
|
|
await agent.api.app.bsky.graph.muteActorList(
|
|
{
|
|
list: listUri,
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphMuteActorList,
|
|
),
|
|
},
|
|
)
|
|
})
|
|
|
|
it('flags mutes in threads', async () => {
|
|
const res = await agent.api.app.bsky.feed.getPostThread(
|
|
{ depth: 1, uri: sc.posts[alice][1].ref.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyFeedGetPostThread,
|
|
),
|
|
},
|
|
)
|
|
expect(forSnapshot(res.data.thread)).toMatchSnapshot()
|
|
})
|
|
|
|
it('does not show reposted content from a muted account in author feed', async () => {
|
|
await sc.repost(alice, sc.posts[carol][0].ref)
|
|
await network.processAll()
|
|
|
|
const res = await agent.api.app.bsky.feed.getAuthorFeed(
|
|
{ actor: alice },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyFeedGetAuthorFeed,
|
|
),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('removes content from muted users on getTimeline', async () => {
|
|
const res = await agent.api.app.bsky.feed.getTimeline(
|
|
{ limit: 100 },
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('removes content from muted users on getListFeed', async () => {
|
|
const listRef = await sc.createList(bob, 'test list', 'curate')
|
|
await sc.addToList(alice, bob, listRef)
|
|
await sc.addToList(alice, carol, listRef)
|
|
await sc.addToList(alice, dan, listRef)
|
|
const res = await agent.api.app.bsky.feed.getListFeed(
|
|
{ list: listRef.uriStr },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
alice,
|
|
ids.AppBskyFeedGetListFeed,
|
|
),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('returns mute status on getProfile', async () => {
|
|
const res = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: carol },
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),
|
|
},
|
|
)
|
|
expect(res.data.viewer?.muted).toBe(true)
|
|
expect(res.data.viewer?.mutedByList?.uri).toBe(listUri)
|
|
})
|
|
|
|
it('returns mute status on getProfiles', async () => {
|
|
const res = await agent.api.app.bsky.actor.getProfiles(
|
|
{ actors: [alice, carol] },
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfiles),
|
|
},
|
|
)
|
|
expect(res.data.profiles[0].viewer?.muted).toBe(false)
|
|
expect(res.data.profiles[0].viewer?.mutedByList).toBeUndefined()
|
|
expect(res.data.profiles[1].viewer?.muted).toBe(true)
|
|
expect(res.data.profiles[1].viewer?.mutedByList?.uri).toEqual(listUri)
|
|
})
|
|
|
|
it('ignores self-mutes', async () => {
|
|
const res = await agent.api.app.bsky.actor.getProfile(
|
|
{ actor: dan }, // dan subscribes to list that contains himself
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyActorGetProfile),
|
|
},
|
|
)
|
|
expect(res.data.viewer?.muted).toBe(false)
|
|
expect(res.data.viewer?.mutedByList).toBeUndefined()
|
|
})
|
|
|
|
it('does not return notifs for muted accounts', async () => {
|
|
const res = await agent.api.app.bsky.notification.listNotifications(
|
|
{
|
|
limit: 100,
|
|
},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyNotificationListNotifications,
|
|
),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.notifications.some((notif) =>
|
|
[bob, carol].includes(notif.author.did),
|
|
),
|
|
).toBeFalsy()
|
|
})
|
|
|
|
it('flags muted accounts in get suggestions', async () => {
|
|
// unfollow so they _would_ show up in suggestions if not for mute
|
|
await sc.unfollow(dan, carol)
|
|
await network.processAll()
|
|
|
|
const res = await agent.api.app.bsky.actor.getSuggestions(
|
|
{
|
|
limit: 100,
|
|
},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyActorGetSuggestions,
|
|
),
|
|
},
|
|
)
|
|
for (const actor of res.data.actors) {
|
|
if ([bob, carol].includes(actor.did)) {
|
|
expect(actor.viewer?.muted).toBe(true)
|
|
expect(actor.viewer?.mutedByList?.uri).toEqual(listUri)
|
|
} else {
|
|
expect(actor.viewer?.muted).toBe(false)
|
|
expect(actor.viewer?.mutedByList).toBeUndefined()
|
|
}
|
|
}
|
|
})
|
|
|
|
it('returns the contents of a list', async () => {
|
|
const res = await agent.api.app.bsky.graph.getList(
|
|
{ list: listUri },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
|
|
)
|
|
expect(forSnapshot(res.data)).toMatchSnapshot()
|
|
})
|
|
|
|
it('paginates getList', async () => {
|
|
const full = await agent.api.app.bsky.graph.getList(
|
|
{ list: listUri },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
|
|
)
|
|
const first = await agent.api.app.bsky.graph.getList(
|
|
{ list: listUri, limit: 1 },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
|
|
)
|
|
const second = await agent.api.app.bsky.graph.getList(
|
|
{ list: listUri, cursor: first.data.cursor },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetList) },
|
|
)
|
|
const combined = [...first.data.items, ...second.data.items]
|
|
expect(combined).toEqual(full.data.items)
|
|
})
|
|
|
|
let otherListUri: string
|
|
|
|
it('returns lists associated with a user', async () => {
|
|
const listRes = await pdsAgent.api.app.bsky.graph.list.create(
|
|
{ repo: alice },
|
|
{
|
|
name: 'new list',
|
|
purpose: 'app.bsky.graph.defs#modlist',
|
|
description: 'blah blah',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
otherListUri = listRes.uri
|
|
|
|
await network.processAll()
|
|
|
|
const res = await agent.api.app.bsky.graph.getLists(
|
|
{ actor: alice },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
|
|
)
|
|
expect(forSnapshot(res.data)).toMatchSnapshot()
|
|
})
|
|
|
|
it('paginates getLists', async () => {
|
|
const full = await agent.api.app.bsky.graph.getLists(
|
|
{ actor: alice },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
|
|
)
|
|
const first = await agent.api.app.bsky.graph.getLists(
|
|
{ actor: alice, limit: 1 },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
|
|
)
|
|
const second = await agent.api.app.bsky.graph.getLists(
|
|
{ actor: alice, cursor: first.data.cursor },
|
|
{ headers: await network.serviceHeaders(dan, ids.AppBskyGraphGetLists) },
|
|
)
|
|
const combined = [...first.data.lists, ...second.data.lists]
|
|
expect(combined).toEqual(full.data.lists)
|
|
})
|
|
|
|
it('returns a users own list mutes', async () => {
|
|
await agent.api.app.bsky.graph.muteActorList(
|
|
{
|
|
list: otherListUri,
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphMuteActorList,
|
|
),
|
|
},
|
|
)
|
|
|
|
const res = await agent.api.app.bsky.graph.getListMutes(
|
|
{},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphGetListMutes,
|
|
),
|
|
},
|
|
)
|
|
expect(forSnapshot(res.data)).toMatchSnapshot()
|
|
})
|
|
|
|
it('paginates getListMutes', async () => {
|
|
const full = await agent.api.app.bsky.graph.getListMutes(
|
|
{},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphGetListMutes,
|
|
),
|
|
},
|
|
)
|
|
const first = await agent.api.app.bsky.graph.getListMutes(
|
|
{ limit: 1 },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphGetListMutes,
|
|
),
|
|
},
|
|
)
|
|
const second = await agent.api.app.bsky.graph.getListMutes(
|
|
{ cursor: first.data.cursor },
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphGetListMutes,
|
|
),
|
|
},
|
|
)
|
|
const combined = [...first.data.lists, ...second.data.lists]
|
|
expect(combined).toEqual(full.data.lists)
|
|
})
|
|
|
|
it('allows unsubscribing from a mute list', async () => {
|
|
await agent.api.app.bsky.graph.unmuteActorList(
|
|
{
|
|
list: otherListUri,
|
|
},
|
|
{
|
|
encoding: 'application/json',
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphUnmuteActorList,
|
|
),
|
|
},
|
|
)
|
|
|
|
const res = await agent.api.app.bsky.graph.getListMutes(
|
|
{},
|
|
{
|
|
headers: await network.serviceHeaders(
|
|
dan,
|
|
ids.AppBskyGraphGetListMutes,
|
|
),
|
|
},
|
|
)
|
|
expect(res.data.lists.length).toBe(1)
|
|
})
|
|
|
|
it('updates list', async () => {
|
|
const uri = new AtUri(listUri)
|
|
await pdsAgent.api.com.atproto.repo.putRecord(
|
|
{
|
|
repo: uri.hostname,
|
|
collection: uri.collection,
|
|
rkey: uri.rkey,
|
|
record: {
|
|
name: 'updated alice mutes',
|
|
purpose: 'app.bsky.graph.defs#modlist',
|
|
description: 'new descript',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
|
|
await network.processAll()
|
|
|
|
const got = await agent.api.app.bsky.graph.getList(
|
|
{ list: listUri },
|
|
{ headers: await network.serviceHeaders(alice, ids.AppBskyGraphGetList) },
|
|
)
|
|
expect(got.data.list.name).toBe('updated alice mutes')
|
|
expect(got.data.list.description).toBe('new descript')
|
|
expect(got.data.list.avatar).toBeUndefined()
|
|
expect(got.data.items.length).toBe(3)
|
|
})
|
|
|
|
it('embeds lists in posts', async () => {
|
|
const postRef = await sc.post(
|
|
alice,
|
|
'list embed!',
|
|
undefined,
|
|
undefined,
|
|
new RecordRef(listUri, listCid),
|
|
)
|
|
await network.processAll()
|
|
const res = await agent.api.app.bsky.feed.getPosts(
|
|
{ uris: [postRef.ref.uriStr] },
|
|
{ headers: await network.serviceHeaders(alice, ids.AppBskyFeedGetPosts) },
|
|
)
|
|
expect(res.data.posts.length).toBe(1)
|
|
expect(forSnapshot(res.data.posts[0])).toMatchSnapshot()
|
|
})
|
|
|
|
it('does not apply "curate" blocklists', async () => {
|
|
const parsedUri = new AtUri(listUri)
|
|
await pdsAgent.api.com.atproto.repo.putRecord(
|
|
{
|
|
repo: parsedUri.hostname,
|
|
collection: parsedUri.collection,
|
|
rkey: parsedUri.rkey,
|
|
record: {
|
|
name: 'curate list',
|
|
purpose: 'app.bsky.graph.defs#curatelist',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
await network.processAll()
|
|
|
|
const res = await agent.api.app.bsky.feed.getTimeline(
|
|
{ limit: 100 },
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),
|
|
).toBeTruthy()
|
|
})
|
|
|
|
it('does not apply deleted blocklists (whose items are still around)', async () => {
|
|
const parsedUri = new AtUri(listUri)
|
|
await pdsAgent.api.app.bsky.graph.list.delete(
|
|
{
|
|
repo: parsedUri.hostname,
|
|
rkey: parsedUri.rkey,
|
|
},
|
|
sc.getHeaders(alice),
|
|
)
|
|
await network.processAll()
|
|
|
|
const res = await agent.api.app.bsky.feed.getTimeline(
|
|
{ limit: 100 },
|
|
{
|
|
headers: await network.serviceHeaders(dan, ids.AppBskyFeedGetTimeline),
|
|
},
|
|
)
|
|
expect(
|
|
res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)),
|
|
).toBeTruthy()
|
|
})
|
|
})
|