From 7c6d816622cf84ae15c7dbf95e1c9a84943aa75f Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Tue, 17 Dec 2024 22:50:51 +0000 Subject: [PATCH 1/3] :sparkles: Add && separator for tags filter param on queryStatuses endpoint --- .../tools/ozone/moderation/queryStatuses.json | 4 +- packages/api/src/client/lexicons.ts | 3 ++ packages/ozone/src/lexicon/lexicons.ts | 3 ++ packages/ozone/src/mod-service/index.ts | 40 ++++++++++++++++--- .../tests/moderation-status-tags.test.ts | 37 +++++++++++++++++ packages/pds/src/lexicon/lexicons.ts | 3 ++ 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/lexicons/tools/ozone/moderation/queryStatuses.json b/lexicons/tools/ozone/moderation/queryStatuses.json index b8bc48df44d..13d2a28f067 100644 --- a/lexicons/tools/ozone/moderation/queryStatuses.json +++ b/lexicons/tools/ozone/moderation/queryStatuses.json @@ -119,7 +119,9 @@ "tags": { "type": "array", "items": { - "type": "string" + "type": "string", + "maxLength": 25, + "description": "Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters" } }, "excludeTags": { diff --git a/packages/api/src/client/lexicons.ts b/packages/api/src/client/lexicons.ts index 32bd89f98a4..0e21313d62b 100644 --- a/packages/api/src/client/lexicons.ts +++ b/packages/api/src/client/lexicons.ts @@ -12498,6 +12498,9 @@ export const schemaDict = { type: 'array', items: { type: 'string', + maxLength: 25, + description: + 'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters', }, }, excludeTags: { diff --git a/packages/ozone/src/lexicon/lexicons.ts b/packages/ozone/src/lexicon/lexicons.ts index 32bd89f98a4..0e21313d62b 100644 --- a/packages/ozone/src/lexicon/lexicons.ts +++ b/packages/ozone/src/lexicon/lexicons.ts @@ -12498,6 +12498,9 @@ export const schemaDict = { type: 'array', items: { type: 'string', + maxLength: 25, + description: + 'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters', }, }, excludeTags: { diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index eeb6aea5848..02f47f71851 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -1,5 +1,5 @@ import net from 'node:net' -import { Insertable, sql } from 'kysely' +import { Insertable, SelectQueryBuilder, sql } from 'kysely' import { CID } from 'multiformats/cid' import { AtUri, INVALID_HANDLE } from '@atproto/syntax' import { InvalidRequestError } from '@atproto/xrpc-server' @@ -791,6 +791,38 @@ export class ModerationService { return result } + applyTagFilter = ( + builder: SelectQueryBuilder, + tags: string[], + ) => { + const { ref } = this.db.db.dynamic + // Build an array of conditions + const conditions = tags.map((tag) => { + if (tag.includes('&&')) { + // Split by '&&' for AND logic + const subTags = tag + .split('&&') + // Make sure spaces on either sides of '&&' are trimmed + .map((subTag) => subTag.trim()) + // Remove empty strings after trimming is applied + .filter(Boolean) + return sql`(${sql.join( + subTags.map( + (subTag) => + sql`${ref('moderation_subject_status.tags')} ? ${subTag}`, + ), + sql` AND `, + )})` + } else { + // Single tag condition + return sql`${ref('moderation_subject_status.tags')} ? ${tag}` + } + }) + + // Combine all conditions with OR + return builder.where(sql`(${sql.join(conditions, sql` OR `)})`) + } + async getSubjectStatuses({ includeAllUserRecords, cursor, @@ -958,11 +990,7 @@ export class ModerationService { } if (tags.length) { - builder = builder.where( - sql`${ref('moderation_subject_status.tags')} ?| array[${sql.join( - tags, - )}]::TEXT[]`, - ) + builder = this.applyTagFilter(builder, tags) } if (excludeTags.length) { diff --git a/packages/ozone/tests/moderation-status-tags.test.ts b/packages/ozone/tests/moderation-status-tags.test.ts index 95aa7c7c8da..51ebc3186e3 100644 --- a/packages/ozone/tests/moderation-status-tags.test.ts +++ b/packages/ozone/tests/moderation-status-tags.test.ts @@ -71,5 +71,42 @@ describe('moderation-status-tags', () => { 'follow-churn', ) }) + + it('allows filtering by tags', async () => { + await modClient.emitEvent({ + subject: { + $type: 'com.atproto.admin.defs#repoRef', + did: sc.dids.alice, + }, + event: { + $type: 'tools.ozone.moderation.defs#modEventTag', + add: ['report:spam', 'lang:ja', 'lang:en'], + remove: [], + }, + }) + const [englishAndJapaneseQueue, englishOrJapaneseQueue] = + await Promise.all([ + modClient.queryStatuses({ + tags: ['lang:ja&&lang:en'], + }), + modClient.queryStatuses({ + tags: ['report:ja', 'lang:en'], + }), + ]) + + // Verify that the queue only contains 1 item with both en and ja tags which is alice's account + expect(englishAndJapaneseQueue.subjectStatuses.length).toEqual(1) + expect(englishAndJapaneseQueue.subjectStatuses[0].subject.did).toEqual( + sc.dids.alice, + ) + + // Verify that when querying for either en or ja tags, both alice and bob are returned + expect(englishOrJapaneseQueue.subjectStatuses.length).toEqual(2) + const englishOrJapaneseDids = englishOrJapaneseQueue.subjectStatuses.map( + ({ subject }) => subject.did, + ) + expect(englishOrJapaneseDids).toContain(sc.dids.alice) + expect(englishOrJapaneseDids).toContain(sc.dids.bob) + }) }) }) diff --git a/packages/pds/src/lexicon/lexicons.ts b/packages/pds/src/lexicon/lexicons.ts index 32bd89f98a4..0e21313d62b 100644 --- a/packages/pds/src/lexicon/lexicons.ts +++ b/packages/pds/src/lexicon/lexicons.ts @@ -12498,6 +12498,9 @@ export const schemaDict = { type: 'array', items: { type: 'string', + maxLength: 25, + description: + 'Items in this array are applied with OR filters. To apply AND filter, put all tags in the same string and separate using && characters', }, }, excludeTags: { From 460076e672c9191f3dc0b21aadf810e8cebee111 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Wed, 18 Dec 2024 22:36:15 +0000 Subject: [PATCH 2/3] :bug: Handle potential empty tag search input --- packages/ozone/src/mod-service/index.ts | 47 ++++++++++++++----------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index 02f47f71851..c59a8e75c86 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -797,27 +797,32 @@ export class ModerationService { ) => { const { ref } = this.db.db.dynamic // Build an array of conditions - const conditions = tags.map((tag) => { - if (tag.includes('&&')) { - // Split by '&&' for AND logic - const subTags = tag - .split('&&') - // Make sure spaces on either sides of '&&' are trimmed - .map((subTag) => subTag.trim()) - // Remove empty strings after trimming is applied - .filter(Boolean) - return sql`(${sql.join( - subTags.map( - (subTag) => - sql`${ref('moderation_subject_status.tags')} ? ${subTag}`, - ), - sql` AND `, - )})` - } else { - // Single tag condition - return sql`${ref('moderation_subject_status.tags')} ? ${tag}` - } - }) + const conditions = tags + .map((tag) => { + if (tag.includes('&&')) { + // Split by '&&' for AND logic + const subTags = tag + .split('&&') + // Make sure spaces on either sides of '&&' are trimmed + .map((subTag) => subTag.trim()) + // Remove empty strings after trimming is applied + .filter(Boolean) + + if (!subTags.length) return null + + return sql`(${sql.join( + subTags.map( + (subTag) => + sql`${ref('moderation_subject_status.tags')} ? ${subTag}`, + ), + sql` AND `, + )})` + } else { + // Single tag condition + return sql`${ref('moderation_subject_status.tags')} ? ${tag}` + } + }) + .filter(Boolean) // Combine all conditions with OR return builder.where(sql`(${sql.join(conditions, sql` OR `)})`) From 09db277e130e05bbe6c5651359f7f7d87ddeb5b0 Mon Sep 17 00:00:00 2001 From: Foysal Ahamed Date: Wed, 18 Dec 2024 22:38:07 +0000 Subject: [PATCH 3/3] :bug: Handle empty condition --- packages/ozone/src/mod-service/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ozone/src/mod-service/index.ts b/packages/ozone/src/mod-service/index.ts index c59a8e75c86..825dc721ae7 100644 --- a/packages/ozone/src/mod-service/index.ts +++ b/packages/ozone/src/mod-service/index.ts @@ -824,6 +824,8 @@ export class ModerationService { }) .filter(Boolean) + if (!conditions.length) return builder + // Combine all conditions with OR return builder.where(sql`(${sql.join(conditions, sql` OR `)})`) }