diff --git a/packages/bsky/src/db/util.ts b/packages/bsky/src/db/util.ts index b8269ba08ac..cee5d100f3d 100644 --- a/packages/bsky/src/db/util.ts +++ b/packages/bsky/src/db/util.ts @@ -1,6 +1,7 @@ import { DummyDriver, DynamicModule, + ExpressionBuilder, RawBuilder, SelectQueryBuilder, sql, @@ -8,7 +9,7 @@ import { SqliteIntrospector, SqliteQueryCompiler, } from 'kysely' -import DatabaseSchema from './database-schema' +import DatabaseSchema, { DatabaseSchemaType } from './database-schema' export const actorWhereClause = (actor: string) => { if (actor.startsWith('did:')) { @@ -58,4 +59,6 @@ export const dummyDialect = { export type DbRef = RawBuilder | ReturnType +export type Subquery = ExpressionBuilder + export type AnyQb = SelectQueryBuilder diff --git a/packages/bsky/src/services/graph/index.ts b/packages/bsky/src/services/graph/index.ts index b154a8c47bb..bc6d3d05677 100644 --- a/packages/bsky/src/services/graph/index.ts +++ b/packages/bsky/src/services/graph/index.ts @@ -1,7 +1,7 @@ import { sql } from 'kysely' import { Database } from '../../db' import { ImageUriBuilder } from '../../image/uri' -import { valuesList } from '../../db/util' +import { DbRef, Subquery, valuesList } from '../../db/util' import { ListInfo } from './types' import { ActorInfoMap } from '../actor' import { @@ -143,6 +143,9 @@ export class GraphService { .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') .whereRef('list_block.creator', '=', sourceRef) .whereRef('list_item.subjectDid', '=', targetRef) + .whereExists((qb) => + this.modListSubquery(qb, ref('list_item.listUri')), + ) .select('list_item.listUri') .limit(1) .as('blockingViaList'), @@ -157,6 +160,9 @@ export class GraphService { .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') .whereRef('list_block.creator', '=', targetRef) .whereRef('list_item.subjectDid', '=', sourceRef) + .whereExists((qb) => + this.modListSubquery(qb, ref('list_item.listUri')), + ) .select('list_item.listUri') .limit(1) .as('blockedByViaList'), @@ -171,6 +177,9 @@ export class GraphService { .innerJoin('list_mute', 'list_mute.listUri', 'list_item.listUri') .whereRef('list_mute.mutedByDid', '=', sourceRef) .whereRef('list_item.subjectDid', '=', targetRef) + .whereExists((qb) => + this.modListSubquery(qb, ref('list_item.listUri')), + ) .select('list_item.listUri') .limit(1) .as('mutingViaList'), @@ -181,6 +190,14 @@ export class GraphService { return result } + modListSubquery(qb: Subquery, ref: DbRef) { + return qb + .selectFrom('list') + .select('uri') + .where('list.purpose', '=', 'app.bsky.graph.defs#modlist') + .whereRef('list.uri', '=', ref) + } + async getBlockState(pairs: RelationshipPair[], bam?: BlockAndMuteState) { pairs = bam ? pairs.filter((pair) => !bam.has(pair)) : pairs const result = bam ?? new BlockAndMuteState() @@ -205,6 +222,9 @@ export class GraphService { .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') .whereRef('list_block.creator', '=', sourceRef) .whereRef('list_item.subjectDid', '=', targetRef) + .whereExists((qb) => + this.modListSubquery(qb, ref('list_item.listUri')), + ) .select('list_item.listUri') .limit(1) .as('blockingViaList'), @@ -219,6 +239,9 @@ export class GraphService { .innerJoin('list_block', 'list_block.subjectUri', 'list_item.listUri') .whereRef('list_block.creator', '=', targetRef) .whereRef('list_item.subjectDid', '=', sourceRef) + .whereExists((qb) => + this.modListSubquery(qb, ref('list_item.listUri')), + ) .select('list_item.listUri') .limit(1) .as('blockedByViaList'), diff --git a/packages/bsky/tests/views/block-lists.test.ts b/packages/bsky/tests/views/block-lists.test.ts index d64927c2378..85339e74cd6 100644 --- a/packages/bsky/tests/views/block-lists.test.ts +++ b/packages/bsky/tests/views/block-lists.test.ts @@ -1,4 +1,4 @@ -import AtpAgent from '@atproto/api' +import AtpAgent, { AtUri } from '@atproto/api' import { TestNetwork, SeedClient, RecordRef, basicSeed } from '@atproto/dev-env' import { forSnapshot } from '../_util' import { BlockedActorError } from '@atproto/api/src/client/types/app/bsky/feed/getAuthorFeed' @@ -430,4 +430,70 @@ describe('pds views with blocking from block lists', () => { const combined = [...first.data.lists, ...second.data.lists] expect(combined).toEqual(full.data.lists) }) + + 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 resCarol = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resCarol.data.feed.some((post) => post.post.author.did === dan), + ).toBeTruthy() + + const resDan = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(dan) }, + ) + expect( + resDan.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 resCarol = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(carol) }, + ) + expect( + resCarol.data.feed.some((post) => post.post.author.did === dan), + ).toBeTruthy() + + const resDan = await agent.api.app.bsky.feed.getTimeline( + { limit: 100 }, + { headers: await network.serviceHeaders(dan) }, + ) + expect( + resDan.data.feed.some((post) => + [bob, carol].includes(post.post.author.did), + ), + ).toBeTruthy() + }) }) diff --git a/packages/bsky/tests/views/mute-lists.test.ts b/packages/bsky/tests/views/mute-lists.test.ts index bd8242d3b48..c366b9bf390 100644 --- a/packages/bsky/tests/views/mute-lists.test.ts +++ b/packages/bsky/tests/views/mute-lists.test.ts @@ -379,4 +379,50 @@ describe('bsky views with mutes from mute lists', () => { 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) }, + ) + 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) }, + ) + expect( + res.data.feed.some((post) => [bob, carol].includes(post.post.author.did)), + ).toBeTruthy() + }) })