@atproto/ozone 0.1.68 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/api/moderation/queryStatuses.js +1 -33
- package/dist/api/moderation/queryStatuses.js.map +1 -1
- package/dist/background.d.ts +49 -6
- package/dist/background.d.ts.map +1 -1
- package/dist/background.js +149 -14
- package/dist/background.js.map +1 -1
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +1 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +1 -0
- package/dist/config/env.js.map +1 -1
- package/dist/daemon/context.d.ts +9 -3
- package/dist/daemon/context.d.ts.map +1 -1
- package/dist/daemon/context.js +33 -3
- package/dist/daemon/context.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +3 -6
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/materialized-view-refresher.d.ts +5 -0
- package/dist/daemon/materialized-view-refresher.d.ts.map +1 -0
- package/dist/daemon/materialized-view-refresher.js +29 -0
- package/dist/daemon/materialized-view-refresher.js.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts +5 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.d.ts.map +1 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js +158 -0
- package/dist/db/migrations/20241220T144630860Z-stats-materialized-views.js.map +1 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +2 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/account_events_stats.d.ts +15 -0
- package/dist/db/schema/account_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_events_stats.js +5 -0
- package/dist/db/schema/account_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_events_stats.d.ts +15 -0
- package/dist/db/schema/account_record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_events_stats.js +5 -0
- package/dist/db/schema/account_record_events_stats.js.map +1 -0
- package/dist/db/schema/account_record_status_stats.d.ts +15 -0
- package/dist/db/schema/account_record_status_stats.d.ts.map +1 -0
- package/dist/db/schema/account_record_status_stats.js +5 -0
- package/dist/db/schema/account_record_status_stats.js.map +1 -0
- package/dist/db/schema/index.d.ts +5 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/record_events_stats.d.ts +14 -0
- package/dist/db/schema/record_events_stats.d.ts.map +1 -0
- package/dist/db/schema/record_events_stats.js +5 -0
- package/dist/db/schema/record_events_stats.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/dist/lexicon/index.d.ts +2 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +2 -0
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +230 -2
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +126 -1
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +5 -0
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js +5 -1
- package/dist/lexicon/types/app/bsky/feed/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/generator.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/feed/generator.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/feed/generator.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +40 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +20 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +7 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/mod-service/index.d.ts +4 -62
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +80 -74
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +115 -4
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/status.js +51 -34
- package/dist/mod-service/status.js.map +1 -1
- package/dist/mod-service/types.d.ts +16 -1
- package/dist/mod-service/types.d.ts.map +1 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +49 -41
- package/dist/mod-service/views.js.map +1 -1
- package/dist/util.d.ts +34 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +132 -0
- package/dist/util.js.map +1 -1
- package/package.json +3 -3
- package/src/api/moderation/queryStatuses.ts +1 -63
- package/src/background.ts +140 -14
- package/src/config/config.ts +2 -0
- package/src/config/env.ts +4 -0
- package/src/daemon/context.ts +43 -5
- package/src/daemon/index.ts +3 -6
- package/src/daemon/materialized-view-refresher.ts +27 -0
- package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +218 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/account_events_stats.ts +16 -0
- package/src/db/schema/account_record_events_stats.ts +15 -0
- package/src/db/schema/account_record_status_stats.ts +15 -0
- package/src/db/schema/index.ts +10 -1
- package/src/db/schema/record_events_stats.ts +15 -0
- package/src/index.ts +1 -7
- package/src/lexicon/index.ts +2 -0
- package/src/lexicon/lexicons.ts +138 -1
- package/src/lexicon/types/app/bsky/feed/defs.ts +9 -0
- package/src/lexicon/types/app/bsky/feed/generator.ts +4 -0
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +62 -0
- package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +11 -1
- package/src/mod-service/index.ts +181 -118
- package/src/mod-service/status.ts +55 -28
- package/src/mod-service/types.ts +22 -1
- package/src/mod-service/views.ts +64 -50
- package/src/util.ts +145 -0
- package/tests/__snapshots__/get-record.test.ts.snap +28 -0
- package/tests/__snapshots__/get-records.test.ts.snap +14 -0
- package/tests/__snapshots__/get-repo.test.ts.snap +11 -0
- package/tests/__snapshots__/get-repos.test.ts.snap +11 -0
- package/tests/__snapshots__/moderation-events.test.ts.snap +19 -0
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +114 -0
- package/tests/get-record.test.ts +4 -0
- package/tests/get-records.test.ts +4 -0
- package/tests/get-repo.test.ts +4 -0
- package/tests/get-repos.test.ts +4 -0
- package/tests/moderation-events.test.ts +4 -0
- package/tests/moderation-statuses.test.ts +4 -0
- package/tests/query-labels.test.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/mod-service/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import net from 'node:net'
|
|
2
|
-
import { Insertable,
|
|
2
|
+
import { Insertable, sql } from 'kysely'
|
|
3
3
|
import { CID } from 'multiformats/cid'
|
|
4
4
|
import { AtUri, INVALID_HANDLE } from '@atproto/syntax'
|
|
5
5
|
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
@@ -29,16 +29,19 @@ import { RepoRef, RepoBlobRef } from '../lexicon/types/com/atproto/admin/defs'
|
|
|
29
29
|
import {
|
|
30
30
|
adjustModerationSubjectStatus,
|
|
31
31
|
getStatusIdentifierFromSubject,
|
|
32
|
+
moderationSubjectStatusQueryBuilder,
|
|
32
33
|
} from './status'
|
|
33
34
|
import {
|
|
34
35
|
ModEventType,
|
|
35
36
|
ModerationEventRow,
|
|
36
37
|
ModerationSubjectStatusRow,
|
|
38
|
+
ModerationSubjectStatusRowWithHandle,
|
|
37
39
|
ReversibleModerationEvent,
|
|
38
40
|
} from './types'
|
|
39
41
|
import { ModerationEvent } from '../db/schema/moderation_event'
|
|
40
42
|
import { StatusKeyset, TimeIdKeyset, paginate } from '../db/pagination'
|
|
41
43
|
import { Label } from '../lexicon/types/com/atproto/label/defs'
|
|
44
|
+
import { QueryParams as QueryStatusParams } from '../lexicon/types/tools/ozone/moderation/queryStatuses'
|
|
42
45
|
import {
|
|
43
46
|
ModSubject,
|
|
44
47
|
RecordSubject,
|
|
@@ -56,6 +59,7 @@ import { httpLogger as log } from '../logger'
|
|
|
56
59
|
import { OzoneConfig } from '../config'
|
|
57
60
|
import { LABELER_HEADER_NAME, ParsedLabelers } from '../util'
|
|
58
61
|
import { ids } from '../lexicon/lexicons'
|
|
62
|
+
import { getReviewState } from '../api/util'
|
|
59
63
|
|
|
60
64
|
export type ModerationServiceCreator = (db: Database) => ModerationService
|
|
61
65
|
|
|
@@ -805,45 +809,6 @@ export class ModerationService {
|
|
|
805
809
|
return result
|
|
806
810
|
}
|
|
807
811
|
|
|
808
|
-
applyTagFilter = (
|
|
809
|
-
builder: SelectQueryBuilder<any, any, any>,
|
|
810
|
-
tags: string[],
|
|
811
|
-
) => {
|
|
812
|
-
const { ref } = this.db.db.dynamic
|
|
813
|
-
// Build an array of conditions
|
|
814
|
-
const conditions = tags
|
|
815
|
-
.map((tag) => {
|
|
816
|
-
if (tag.includes('&&')) {
|
|
817
|
-
// Split by '&&' for AND logic
|
|
818
|
-
const subTags = tag
|
|
819
|
-
.split('&&')
|
|
820
|
-
// Make sure spaces on either sides of '&&' are trimmed
|
|
821
|
-
.map((subTag) => subTag.trim())
|
|
822
|
-
// Remove empty strings after trimming is applied
|
|
823
|
-
.filter(Boolean)
|
|
824
|
-
|
|
825
|
-
if (!subTags.length) return null
|
|
826
|
-
|
|
827
|
-
return sql`(${sql.join(
|
|
828
|
-
subTags.map(
|
|
829
|
-
(subTag) =>
|
|
830
|
-
sql`${ref('moderation_subject_status.tags')} ? ${subTag}`,
|
|
831
|
-
),
|
|
832
|
-
sql` AND `,
|
|
833
|
-
)})`
|
|
834
|
-
} else {
|
|
835
|
-
// Single tag condition
|
|
836
|
-
return sql`${ref('moderation_subject_status.tags')} ? ${tag}`
|
|
837
|
-
}
|
|
838
|
-
})
|
|
839
|
-
.filter(Boolean)
|
|
840
|
-
|
|
841
|
-
if (!conditions.length) return builder
|
|
842
|
-
|
|
843
|
-
// Combine all conditions with OR
|
|
844
|
-
return builder.where(sql`(${sql.join(conditions, sql` OR `)})`)
|
|
845
|
-
}
|
|
846
|
-
|
|
847
812
|
async getSubjectStatuses({
|
|
848
813
|
queueCount,
|
|
849
814
|
queueIndex,
|
|
@@ -858,54 +823,31 @@ export class ModerationService {
|
|
|
858
823
|
reviewedBefore,
|
|
859
824
|
reportedAfter,
|
|
860
825
|
reportedBefore,
|
|
861
|
-
includeMuted,
|
|
826
|
+
includeMuted = false,
|
|
862
827
|
hostingDeletedBefore,
|
|
863
828
|
hostingDeletedAfter,
|
|
864
829
|
hostingUpdatedBefore,
|
|
865
830
|
hostingUpdatedAfter,
|
|
866
831
|
hostingStatuses,
|
|
867
|
-
onlyMuted,
|
|
832
|
+
onlyMuted = false,
|
|
868
833
|
ignoreSubjects,
|
|
869
|
-
sortDirection,
|
|
834
|
+
sortDirection = 'desc',
|
|
870
835
|
lastReviewedBy,
|
|
871
|
-
sortField,
|
|
836
|
+
sortField = 'lastReportedAt',
|
|
872
837
|
subject,
|
|
873
838
|
tags,
|
|
874
839
|
excludeTags,
|
|
875
840
|
collections,
|
|
876
841
|
subjectType,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
842
|
+
minAccountSuspendCount,
|
|
843
|
+
minReportedRecordsCount,
|
|
844
|
+
minTakendownRecordsCount,
|
|
845
|
+
}: QueryStatusParams): Promise<{
|
|
846
|
+
statuses: ModerationSubjectStatusRowWithHandle[]
|
|
882
847
|
cursor?: string
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
reviewedBefore?: string
|
|
887
|
-
reviewState?: ModerationSubjectStatusRow['reviewState']
|
|
888
|
-
reviewedAfter?: string
|
|
889
|
-
reportedAfter?: string
|
|
890
|
-
reportedBefore?: string
|
|
891
|
-
includeMuted?: boolean
|
|
892
|
-
hostingDeletedBefore?: string
|
|
893
|
-
hostingDeletedAfter?: string
|
|
894
|
-
hostingUpdatedBefore?: string
|
|
895
|
-
hostingUpdatedAfter?: string
|
|
896
|
-
hostingStatuses?: string[]
|
|
897
|
-
onlyMuted?: boolean
|
|
898
|
-
subject?: string
|
|
899
|
-
ignoreSubjects?: string[]
|
|
900
|
-
sortDirection: 'asc' | 'desc'
|
|
901
|
-
lastReviewedBy?: string
|
|
902
|
-
sortField: 'lastReviewedAt' | 'lastReportedAt'
|
|
903
|
-
tags: string[]
|
|
904
|
-
excludeTags: string[]
|
|
905
|
-
collections: string[]
|
|
906
|
-
subjectType?: string
|
|
907
|
-
}) {
|
|
908
|
-
let builder = this.db.db.selectFrom('moderation_subject_status').selectAll()
|
|
848
|
+
}> {
|
|
849
|
+
let builder = moderationSubjectStatusQueryBuilder(this.db.db)
|
|
850
|
+
|
|
909
851
|
const { ref } = this.db.db.dynamic
|
|
910
852
|
|
|
911
853
|
if (subject) {
|
|
@@ -919,14 +861,18 @@ export class ModerationService {
|
|
|
919
861
|
if (!includeAllUserRecords) {
|
|
920
862
|
builder = builder.where((qb) =>
|
|
921
863
|
subjectInfo.recordPath
|
|
922
|
-
? qb.where(
|
|
923
|
-
|
|
864
|
+
? qb.where(
|
|
865
|
+
'moderation_subject_status.recordPath',
|
|
866
|
+
'=',
|
|
867
|
+
subjectInfo.recordPath,
|
|
868
|
+
)
|
|
869
|
+
: qb.where('moderation_subject_status.recordPath', '=', ''),
|
|
924
870
|
)
|
|
925
871
|
}
|
|
926
872
|
} else if (subjectType === 'account') {
|
|
927
|
-
builder = builder.where('recordPath', '=', '')
|
|
873
|
+
builder = builder.where('moderation_subject_status.recordPath', '=', '')
|
|
928
874
|
} else if (subjectType === 'record') {
|
|
929
|
-
builder = builder.where('recordPath', '!=', '')
|
|
875
|
+
builder = builder.where('moderation_subject_status.recordPath', '!=', '')
|
|
930
876
|
}
|
|
931
877
|
|
|
932
878
|
// Only fetch items that belongs to the specified queue when specified
|
|
@@ -948,110 +894,216 @@ export class ModerationService {
|
|
|
948
894
|
}
|
|
949
895
|
|
|
950
896
|
// If subjectType is set to 'account' let that take priority and ignore collections filter
|
|
951
|
-
if (
|
|
952
|
-
builder = builder
|
|
953
|
-
|
|
954
|
-
|
|
897
|
+
if (subjectType !== 'account' && collections?.length) {
|
|
898
|
+
builder = builder
|
|
899
|
+
.where('moderation_subject_status.recordPath', '!=', '')
|
|
900
|
+
.where((qb) => {
|
|
901
|
+
for (const collection of collections) {
|
|
902
|
+
qb = qb.orWhere(
|
|
903
|
+
'moderation_subject_status.recordPath',
|
|
904
|
+
'like',
|
|
905
|
+
`${collection}/%`,
|
|
906
|
+
)
|
|
907
|
+
}
|
|
908
|
+
return qb
|
|
955
909
|
})
|
|
956
|
-
return qb
|
|
957
|
-
})
|
|
958
910
|
}
|
|
959
911
|
|
|
960
912
|
if (ignoreSubjects?.length) {
|
|
961
913
|
builder = builder
|
|
962
|
-
.where('did', 'not in', ignoreSubjects)
|
|
963
|
-
.where('recordPath', 'not in', ignoreSubjects)
|
|
914
|
+
.where('moderation_subject_status.did', 'not in', ignoreSubjects)
|
|
915
|
+
.where('moderation_subject_status.recordPath', 'not in', ignoreSubjects)
|
|
964
916
|
}
|
|
965
917
|
|
|
966
|
-
|
|
967
|
-
|
|
918
|
+
const reviewStateNormalized = getReviewState(reviewState)
|
|
919
|
+
if (reviewStateNormalized) {
|
|
920
|
+
builder = builder.where(
|
|
921
|
+
'moderation_subject_status.reviewState',
|
|
922
|
+
'=',
|
|
923
|
+
reviewStateNormalized,
|
|
924
|
+
)
|
|
968
925
|
}
|
|
969
926
|
|
|
970
927
|
if (lastReviewedBy) {
|
|
971
|
-
builder = builder.where(
|
|
928
|
+
builder = builder.where(
|
|
929
|
+
'moderation_subject_status.lastReviewedBy',
|
|
930
|
+
'=',
|
|
931
|
+
lastReviewedBy,
|
|
932
|
+
)
|
|
972
933
|
}
|
|
973
934
|
|
|
974
935
|
if (reviewedAfter) {
|
|
975
|
-
builder = builder.where(
|
|
936
|
+
builder = builder.where(
|
|
937
|
+
'moderation_subject_status.lastReviewedAt',
|
|
938
|
+
'>',
|
|
939
|
+
reviewedAfter,
|
|
940
|
+
)
|
|
976
941
|
}
|
|
977
942
|
|
|
978
943
|
if (reviewedBefore) {
|
|
979
|
-
builder = builder.where(
|
|
944
|
+
builder = builder.where(
|
|
945
|
+
'moderation_subject_status.lastReviewedAt',
|
|
946
|
+
'<',
|
|
947
|
+
reviewedBefore,
|
|
948
|
+
)
|
|
980
949
|
}
|
|
981
950
|
|
|
982
951
|
if (hostingUpdatedAfter) {
|
|
983
|
-
builder = builder.where(
|
|
952
|
+
builder = builder.where(
|
|
953
|
+
'moderation_subject_status.hostingUpdatedAt',
|
|
954
|
+
'>',
|
|
955
|
+
hostingUpdatedAfter,
|
|
956
|
+
)
|
|
984
957
|
}
|
|
985
958
|
|
|
986
959
|
if (hostingUpdatedBefore) {
|
|
987
|
-
builder = builder.where(
|
|
960
|
+
builder = builder.where(
|
|
961
|
+
'moderation_subject_status.hostingUpdatedAt',
|
|
962
|
+
'<',
|
|
963
|
+
hostingUpdatedBefore,
|
|
964
|
+
)
|
|
988
965
|
}
|
|
989
966
|
|
|
990
967
|
if (hostingDeletedAfter) {
|
|
991
|
-
builder = builder.where(
|
|
968
|
+
builder = builder.where(
|
|
969
|
+
'moderation_subject_status.hostingDeletedAt',
|
|
970
|
+
'>',
|
|
971
|
+
hostingDeletedAfter,
|
|
972
|
+
)
|
|
992
973
|
}
|
|
993
974
|
|
|
994
975
|
if (hostingDeletedBefore) {
|
|
995
|
-
builder = builder.where(
|
|
976
|
+
builder = builder.where(
|
|
977
|
+
'moderation_subject_status.hostingDeletedAt',
|
|
978
|
+
'<',
|
|
979
|
+
hostingDeletedBefore,
|
|
980
|
+
)
|
|
996
981
|
}
|
|
997
982
|
|
|
998
983
|
if (hostingStatuses?.length) {
|
|
999
|
-
builder = builder.where(
|
|
984
|
+
builder = builder.where(
|
|
985
|
+
'moderation_subject_status.hostingStatus',
|
|
986
|
+
'in',
|
|
987
|
+
hostingStatuses,
|
|
988
|
+
)
|
|
1000
989
|
}
|
|
1001
990
|
|
|
1002
991
|
if (reportedAfter) {
|
|
1003
|
-
builder = builder.where(
|
|
992
|
+
builder = builder.where(
|
|
993
|
+
'moderation_subject_status.lastReviewedAt',
|
|
994
|
+
'>',
|
|
995
|
+
reportedAfter,
|
|
996
|
+
)
|
|
1004
997
|
}
|
|
1005
998
|
|
|
1006
999
|
if (reportedBefore) {
|
|
1007
|
-
builder = builder.where(
|
|
1000
|
+
builder = builder.where(
|
|
1001
|
+
'moderation_subject_status.lastReportedAt',
|
|
1002
|
+
'<',
|
|
1003
|
+
reportedBefore,
|
|
1004
|
+
)
|
|
1008
1005
|
}
|
|
1009
1006
|
|
|
1010
1007
|
if (takendown) {
|
|
1011
|
-
builder = builder.where('takendown', '=', true)
|
|
1008
|
+
builder = builder.where('moderation_subject_status.takendown', '=', true)
|
|
1012
1009
|
}
|
|
1013
1010
|
|
|
1014
1011
|
if (appealed !== undefined) {
|
|
1015
1012
|
builder =
|
|
1016
1013
|
appealed === false
|
|
1017
|
-
? builder.where('appealed', 'is', null)
|
|
1018
|
-
: builder.where('appealed', '=', appealed)
|
|
1014
|
+
? builder.where('moderation_subject_status.appealed', 'is', null)
|
|
1015
|
+
: builder.where('moderation_subject_status.appealed', '=', appealed)
|
|
1019
1016
|
}
|
|
1020
1017
|
|
|
1021
1018
|
if (!includeMuted) {
|
|
1022
1019
|
builder = builder.where((qb) =>
|
|
1023
1020
|
qb
|
|
1024
|
-
.where(
|
|
1025
|
-
|
|
1021
|
+
.where(
|
|
1022
|
+
'moderation_subject_status.muteUntil',
|
|
1023
|
+
'<',
|
|
1024
|
+
new Date().toISOString(),
|
|
1025
|
+
)
|
|
1026
|
+
.orWhere('moderation_subject_status.muteUntil', 'is', null),
|
|
1026
1027
|
)
|
|
1027
1028
|
}
|
|
1028
1029
|
|
|
1029
1030
|
if (onlyMuted) {
|
|
1030
1031
|
builder = builder.where((qb) =>
|
|
1031
1032
|
qb
|
|
1032
|
-
.where(
|
|
1033
|
-
|
|
1033
|
+
.where(
|
|
1034
|
+
'moderation_subject_status.muteUntil',
|
|
1035
|
+
'>',
|
|
1036
|
+
new Date().toISOString(),
|
|
1037
|
+
)
|
|
1038
|
+
.orWhere(
|
|
1039
|
+
'moderation_subject_status.muteReportingUntil',
|
|
1040
|
+
'>',
|
|
1041
|
+
new Date().toISOString(),
|
|
1042
|
+
),
|
|
1034
1043
|
)
|
|
1035
1044
|
}
|
|
1036
1045
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1046
|
+
// ["tag1", "tag2 && tag3", "tag4"] => [["tag1"], ["tag2", "tag3"], ["tag4"]]
|
|
1047
|
+
const conditions = parseTags(tags)
|
|
1048
|
+
if (conditions?.length) {
|
|
1049
|
+
// [["tag1"], ["tag2", "tag3"], ["tag4"]] => (tags ? 'tag1') OR (tags ? 'tag2' AND tags ? 'tag3') OR (tags ? 'tag4')
|
|
1050
|
+
builder = builder.where((qb) => {
|
|
1051
|
+
for (const subTags of conditions) {
|
|
1052
|
+
// OR between every conditions items (subTags)
|
|
1053
|
+
qb = qb.orWhere((qb) => {
|
|
1054
|
+
// AND between every subTags items (subTag)
|
|
1055
|
+
for (const subTag of subTags) {
|
|
1056
|
+
qb = qb.where(
|
|
1057
|
+
sql`${ref('moderation_subject_status.tags')} ? ${subTag}`,
|
|
1058
|
+
)
|
|
1059
|
+
}
|
|
1060
|
+
return qb
|
|
1061
|
+
})
|
|
1062
|
+
}
|
|
1063
|
+
return qb
|
|
1064
|
+
})
|
|
1039
1065
|
}
|
|
1040
1066
|
|
|
1041
|
-
if (excludeTags
|
|
1067
|
+
if (excludeTags?.length) {
|
|
1042
1068
|
builder = builder.where((qb) =>
|
|
1043
1069
|
qb
|
|
1044
1070
|
.where(
|
|
1045
|
-
sql`NOT(${ref(
|
|
1046
|
-
'moderation_subject_status.tags',
|
|
1047
|
-
)} ?| array[${sql.join(excludeTags)}]::TEXT[])`,
|
|
1071
|
+
sql`NOT(${ref('moderation_subject_status.tags')} ?| array[${sql.join(excludeTags)}]::TEXT[])`,
|
|
1048
1072
|
)
|
|
1049
1073
|
.orWhere('tags', 'is', null),
|
|
1050
1074
|
)
|
|
1051
1075
|
}
|
|
1052
1076
|
|
|
1077
|
+
if (minAccountSuspendCount != null && minAccountSuspendCount > 0) {
|
|
1078
|
+
builder = builder.where(
|
|
1079
|
+
'account_events_stats.suspendCount',
|
|
1080
|
+
'>=',
|
|
1081
|
+
minAccountSuspendCount,
|
|
1082
|
+
)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (minTakendownRecordsCount != null && minTakendownRecordsCount > 0) {
|
|
1086
|
+
builder = builder.where(
|
|
1087
|
+
'account_record_status_stats.takendownCount',
|
|
1088
|
+
'>=',
|
|
1089
|
+
minTakendownRecordsCount,
|
|
1090
|
+
)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (minReportedRecordsCount != null && minReportedRecordsCount > 0) {
|
|
1094
|
+
builder = builder.where(
|
|
1095
|
+
'account_record_events_stats.reportedCount',
|
|
1096
|
+
'>=',
|
|
1097
|
+
minReportedRecordsCount,
|
|
1098
|
+
)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1053
1101
|
const keyset = new StatusKeyset(
|
|
1054
|
-
|
|
1102
|
+
sortField === 'reportedRecordsCount'
|
|
1103
|
+
? ref(`account_record_events_stats.reportedCount`)
|
|
1104
|
+
: sortField === 'takendownRecordsCount'
|
|
1105
|
+
? ref(`account_record_status_stats.takendownCount`)
|
|
1106
|
+
: ref(`moderation_subject_status.${sortField}`),
|
|
1055
1107
|
ref('moderation_subject_status.id'),
|
|
1056
1108
|
)
|
|
1057
1109
|
const paginatedBuilder = paginate(builder, {
|
|
@@ -1067,13 +1119,12 @@ export class ModerationService {
|
|
|
1067
1119
|
const infos = await this.views.getAccoutInfosByDid(
|
|
1068
1120
|
results.map((r) => r.did),
|
|
1069
1121
|
)
|
|
1070
|
-
const resultsWithHandles = results.map((r) => ({
|
|
1071
|
-
...r,
|
|
1072
|
-
handle: infos.get(r.did)?.handle ?? INVALID_HANDLE,
|
|
1073
|
-
}))
|
|
1074
1122
|
|
|
1075
1123
|
return {
|
|
1076
|
-
statuses:
|
|
1124
|
+
statuses: results.map((r) => ({
|
|
1125
|
+
...r,
|
|
1126
|
+
handle: infos.get(r.did)?.handle ?? INVALID_HANDLE,
|
|
1127
|
+
})),
|
|
1077
1128
|
cursor: keyset.packFromResult(results),
|
|
1078
1129
|
}
|
|
1079
1130
|
}
|
|
@@ -1195,6 +1246,18 @@ export class ModerationService {
|
|
|
1195
1246
|
}
|
|
1196
1247
|
}
|
|
1197
1248
|
|
|
1249
|
+
const parseTags = (tags?: string[]) =>
|
|
1250
|
+
tags
|
|
1251
|
+
?.map((tag) =>
|
|
1252
|
+
tag
|
|
1253
|
+
.split(/\s*&&\s*/g)
|
|
1254
|
+
.map((subTag) => subTag.trim())
|
|
1255
|
+
// Ignore invalid syntax ("", "tag1 &&", "&& tag2", "tag1 && && tag2", etc.)
|
|
1256
|
+
.filter(Boolean),
|
|
1257
|
+
)
|
|
1258
|
+
// Ignore invalid items
|
|
1259
|
+
.filter((subTags): subTags is [string, ...string[]] => subTags.length > 0)
|
|
1260
|
+
|
|
1198
1261
|
const isSafeUrl = (url: URL) => {
|
|
1199
1262
|
if (url.protocol !== 'https:') return false
|
|
1200
1263
|
if (!url.hostname || url.hostname === 'localhost') return false
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
// This may require better organization but for now, just dumping functions here containing DB queries for moderation status
|
|
2
2
|
|
|
3
|
+
import { HOUR } from '@atproto/common'
|
|
3
4
|
import { AtUri } from '@atproto/syntax'
|
|
4
5
|
import { Database } from '../db'
|
|
5
|
-
import
|
|
6
|
+
import DatabaseSchema from '../db/schema'
|
|
7
|
+
import { jsonb } from '../db/types'
|
|
8
|
+
import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
|
|
6
9
|
import {
|
|
7
|
-
REVIEWOPEN,
|
|
8
10
|
REVIEWCLOSED,
|
|
9
11
|
REVIEWESCALATED,
|
|
10
12
|
REVIEWNONE,
|
|
13
|
+
REVIEWOPEN,
|
|
11
14
|
} from '../lexicon/types/tools/ozone/moderation/defs'
|
|
12
15
|
import { ModerationEventRow, ModerationSubjectStatusRow } from './types'
|
|
13
|
-
import { HOUR } from '@atproto/common'
|
|
14
|
-
import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
|
|
15
|
-
import { jsonb } from '../db/types'
|
|
16
16
|
|
|
17
17
|
const getSubjectStatusForModerationEvent = ({
|
|
18
18
|
currentStatus,
|
|
@@ -203,6 +203,56 @@ const getSubjectStatusForRecordEvent = ({
|
|
|
203
203
|
return {}
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
export const moderationSubjectStatusQueryBuilder = (db: DatabaseSchema) => {
|
|
207
|
+
// @NOTE: Using select() instead of selectAll() below because the materialized
|
|
208
|
+
// views might be incomplete, and we don't want the null `did` columns to
|
|
209
|
+
// interfere with the (never null) `did` column from the
|
|
210
|
+
// `moderation_subject_status` table in the results
|
|
211
|
+
return db
|
|
212
|
+
.selectFrom('moderation_subject_status')
|
|
213
|
+
.selectAll('moderation_subject_status')
|
|
214
|
+
.leftJoin('account_events_stats', (join) =>
|
|
215
|
+
join.onRef(
|
|
216
|
+
'moderation_subject_status.did',
|
|
217
|
+
'=',
|
|
218
|
+
'account_events_stats.subjectDid',
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
.select([
|
|
222
|
+
'account_events_stats.takedownCount',
|
|
223
|
+
'account_events_stats.suspendCount',
|
|
224
|
+
'account_events_stats.escalateCount',
|
|
225
|
+
'account_events_stats.reportCount',
|
|
226
|
+
'account_events_stats.appealCount',
|
|
227
|
+
])
|
|
228
|
+
.leftJoin('account_record_events_stats', (join) =>
|
|
229
|
+
join.onRef(
|
|
230
|
+
'moderation_subject_status.did',
|
|
231
|
+
'=',
|
|
232
|
+
'account_record_events_stats.subjectDid',
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
.select([
|
|
236
|
+
'account_record_events_stats.totalReports',
|
|
237
|
+
'account_record_events_stats.reportedCount',
|
|
238
|
+
'account_record_events_stats.escalatedCount',
|
|
239
|
+
'account_record_events_stats.appealedCount',
|
|
240
|
+
])
|
|
241
|
+
.leftJoin('account_record_status_stats', (join) =>
|
|
242
|
+
join.onRef(
|
|
243
|
+
'moderation_subject_status.did',
|
|
244
|
+
'=',
|
|
245
|
+
'account_record_status_stats.did',
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
.select([
|
|
249
|
+
'account_record_status_stats.subjectCount',
|
|
250
|
+
'account_record_status_stats.pendingCount',
|
|
251
|
+
'account_record_status_stats.processedCount',
|
|
252
|
+
'account_record_status_stats.takendownCount',
|
|
253
|
+
])
|
|
254
|
+
}
|
|
255
|
+
|
|
206
256
|
// Based on a given moderation action event, this function will update the moderation status of the subject
|
|
207
257
|
// If there's no existing status, it will create one
|
|
208
258
|
// If the action event does not affect the status, it will do nothing
|
|
@@ -393,29 +443,6 @@ export const adjustModerationSubjectStatus = async (
|
|
|
393
443
|
return status || null
|
|
394
444
|
}
|
|
395
445
|
|
|
396
|
-
type ModerationSubjectStatusFilter =
|
|
397
|
-
| Pick<ModerationSubjectStatus, 'did'>
|
|
398
|
-
| Pick<ModerationSubjectStatus, 'did' | 'recordPath'>
|
|
399
|
-
| Pick<ModerationSubjectStatus, 'did' | 'recordPath' | 'recordCid'>
|
|
400
|
-
export const getModerationSubjectStatus = async (
|
|
401
|
-
db: Database,
|
|
402
|
-
filters: ModerationSubjectStatusFilter,
|
|
403
|
-
) => {
|
|
404
|
-
let builder = db.db
|
|
405
|
-
.selectFrom('moderation_subject_status')
|
|
406
|
-
// DID will always be passed at the very least
|
|
407
|
-
.where('did', '=', filters.did)
|
|
408
|
-
.where('recordPath', '=', 'recordPath' in filters ? filters.recordPath : '')
|
|
409
|
-
|
|
410
|
-
if ('recordCid' in filters) {
|
|
411
|
-
builder = builder.where('recordCid', '=', filters.recordCid)
|
|
412
|
-
} else {
|
|
413
|
-
builder = builder.where('recordCid', 'is', null)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return builder.executeTakeFirst()
|
|
417
|
-
}
|
|
418
|
-
|
|
419
446
|
export const getStatusIdentifierFromSubject = (
|
|
420
447
|
subject: string | AtUri,
|
|
421
448
|
): { did: string; recordPath: string } => {
|
package/src/mod-service/types.ts
CHANGED
|
@@ -18,8 +18,29 @@ export type ModerationEventRowWithHandle = ModerationEventRow & {
|
|
|
18
18
|
creatorHandle?: string | null
|
|
19
19
|
}
|
|
20
20
|
export type ModerationSubjectStatusRow = Selectable<ModerationSubjectStatus>
|
|
21
|
+
export type ModerationSubjectStatusRowWithStats = ModerationSubjectStatusRow & {
|
|
22
|
+
// account_events_stats
|
|
23
|
+
takedownCount: number | null
|
|
24
|
+
suspendCount: number | null
|
|
25
|
+
escalateCount: number | null
|
|
26
|
+
reportCount: number | null
|
|
27
|
+
appealCount: number | null
|
|
28
|
+
|
|
29
|
+
// account_record_events_stats
|
|
30
|
+
totalReports: number | null
|
|
31
|
+
reportedCount: number | null
|
|
32
|
+
escalatedCount: number | null
|
|
33
|
+
appealedCount: number | null
|
|
34
|
+
|
|
35
|
+
// account_record_status_stats
|
|
36
|
+
subjectCount: number | null
|
|
37
|
+
pendingCount: number | null
|
|
38
|
+
processedCount: number | null
|
|
39
|
+
takendownCount: number | null
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
export type ModerationSubjectStatusRowWithHandle =
|
|
22
|
-
|
|
43
|
+
ModerationSubjectStatusRowWithStats & { handle: string | null }
|
|
23
44
|
|
|
24
45
|
export type ModEventType =
|
|
25
46
|
| ToolsOzoneModerationDefs.ModEventTakedown
|