Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

NCMEC <-> Queue Improvements and default NCMEC queue (#63)

* NCMEC <-> Queue Improvements and default NCMEC queue

* code review changes

authored by

Juan Mrad and committed by
GitHub
5133c83e b4e5959b

+315 -74
+12
.devops/migrator/src/scripts/api-server-pg/2026.02.10T00.00.00.add_default_ncmec_queue_to_ncmec_org_settings.sql
··· 1 + -- Add default NCMEC queue to ncmec_org_settings so Enqueue to NCMEC can target a specific queue. 2 + ALTER TABLE ncmec_reporting.ncmec_org_settings 3 + ADD COLUMN IF NOT EXISTS default_ncmec_queue_id character varying(255) NULL; 4 + 5 + ALTER TABLE ncmec_reporting.ncmec_org_settings 6 + ADD CONSTRAINT ncmec_org_settings_default_ncmec_queue_fkey 7 + FOREIGN KEY (default_ncmec_queue_id) 8 + REFERENCES manual_review_tool.manual_review_queues(id) 9 + ON DELETE SET NULL; 10 + 11 + COMMENT ON COLUMN ncmec_reporting.ncmec_org_settings.default_ncmec_queue_id IS 12 + 'When set, Enqueue to NCMEC sends jobs to this queue instead of the org default.';
+1
README.md
··· 52 52 3. Make sure the `.env` files for `/server` and `.devops/migrator` are populated (including ClickHouse credentials). Run database migrations: 53 53 ```bash 54 54 npm run db:update -- --env staging --db api-server-pg 55 + npm run db:update -- --env staging --db scylla 55 56 npm run db:update -- --env staging --db clickhouse 56 57 ``` 57 58 ### Alternative: Single Command for Steps 2-3
+13
client/src/graphql/generated.ts
··· 2714 2714 readonly __typename: 'NcmecOrgSettings'; 2715 2715 readonly companyTemplate?: Maybe<Scalars['String']>; 2716 2716 readonly contactEmail?: Maybe<Scalars['String']>; 2717 + readonly defaultNcmecQueueId?: Maybe<Scalars['String']>; 2717 2718 readonly legalUrl?: Maybe<Scalars['String']>; 2718 2719 readonly moreInfoUrl?: Maybe<Scalars['String']>; 2719 2720 readonly ncmecAdditionalInfoEndpoint?: Maybe<Scalars['String']>; ··· 2725 2726 export type GQLNcmecOrgSettingsInput = { 2726 2727 readonly companyTemplate?: InputMaybe<Scalars['String']>; 2727 2728 readonly contactEmail?: InputMaybe<Scalars['String']>; 2729 + readonly defaultNcmecQueueId?: InputMaybe<Scalars['String']>; 2728 2730 readonly legalUrl?: InputMaybe<Scalars['String']>; 2729 2731 readonly moreInfoUrl?: InputMaybe<Scalars['String']>; 2730 2732 readonly ncmecAdditionalInfoEndpoint?: InputMaybe<Scalars['String']>; ··· 23585 23587 readonly legalUrl?: string | null; 23586 23588 readonly ncmecPreservationEndpoint?: string | null; 23587 23589 readonly ncmecAdditionalInfoEndpoint?: string | null; 23590 + readonly defaultNcmecQueueId?: string | null; 23588 23591 } | null; 23589 23592 readonly myOrg?: { 23590 23593 readonly __typename: 'Org'; 23591 23594 readonly hasNCMECReportingEnabled: boolean; 23595 + readonly mrtQueues: ReadonlyArray<{ 23596 + readonly __typename: 'ManualReviewQueue'; 23597 + readonly id: string; 23598 + readonly name: string; 23599 + }>; 23592 23600 } | null; 23593 23601 }; 23594 23602 ··· 36506 36514 legalUrl 36507 36515 ncmecPreservationEndpoint 36508 36516 ncmecAdditionalInfoEndpoint 36517 + defaultNcmecQueueId 36509 36518 } 36510 36519 myOrg { 36511 36520 hasNCMECReportingEnabled 36521 + mrtQueues { 36522 + id 36523 + name 36524 + } 36512 36525 } 36513 36526 } 36514 36527 `;
+55 -32
client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobReview.tsx
··· 5 5 import { multilevelListFromFlatList } from '@/utils/tree'; 6 6 import { DownOutlined, LoadingOutlined } from '@ant-design/icons'; 7 7 import { gql } from '@apollo/client'; 8 - import { Button, Dropdown, Input, Select } from 'antd'; 8 + import { Button, Dropdown, Input, Select, Tooltip } from 'antd'; 9 9 import { useCallback, useContext, useEffect, useRef, useState } from 'react'; 10 10 import { Helmet } from 'react-helmet-async'; 11 11 import { useNavigate, useParams } from 'react-router-dom'; ··· 1078 1078 reportHistory={reportHistory} 1079 1079 /> 1080 1080 ) : null; 1081 + const decisionActions = [ 1082 + ...(appealPayloadTypenames.includes(payload.__typename) 1083 + ? builtInAppealActions 1084 + : builtInActions), 1085 + ...(payload.__typename === 'ContentManualReviewJobPayload' || 1086 + payload.__typename === 'UserManualReviewJobPayload' 1087 + ? [ncmecAction] 1088 + : []), 1089 + ...(appealPayloadTypenames.includes(payload.__typename) 1090 + ? [] 1091 + : filteredActions 1092 + .filter( 1093 + (action) => 1094 + action.itemTypes 1095 + .map((itemType) => itemType.id) 1096 + .includes(payload.item.type?.id ?? '') && 1097 + // Transform and move actions should be done through decisions 1098 + action.__typename === 'CustomAction', 1099 + ) 1100 + .sort((a, b) => a.name.localeCompare(b.name))), 1101 + ...builtInMoveAction, 1102 + ]; 1103 + 1081 1104 const actionList = ( 1082 - <div className="sticky flex flex-col overflow-hidden border border-gray-200 border-solid rounded-md shrink-0"> 1083 - {[ 1084 - ...(appealPayloadTypenames.includes(payload.__typename) 1085 - ? builtInAppealActions 1086 - : builtInActions), 1087 - ...(org.hasNCMECReportingEnabled && 1088 - (payload.__typename === 'ContentManualReviewJobPayload' || 1089 - payload.__typename === 'UserManualReviewJobPayload') 1090 - ? [ncmecAction] 1091 - : []), 1092 - ...(appealPayloadTypenames.includes(payload.__typename) 1093 - ? [] 1094 - : filteredActions 1095 - .filter( 1096 - (action) => 1097 - action.itemTypes 1098 - .map((itemType) => itemType.id) 1099 - .includes(payload.item.type.id) && 1100 - // Transform and move actions should be done through decisions 1101 - action.__typename === 'CustomAction', 1102 - ) 1103 - .sort((a, b) => a.name.localeCompare(b.name))), 1104 - ...builtInMoveAction, 1105 - ] 1106 - .map((action) => { 1105 + <div 1106 + className="sticky flex flex-col border border-gray-200 border-solid rounded-md shrink-0" 1107 + data-testid="manual-review-decision-action-list" 1108 + > 1109 + {decisionActions.map((action) => { 1107 1110 const { key, selected, label } = (() => { 1108 1111 if ('type' in action) { 1109 1112 return { ··· 1156 1159 }`} 1157 1160 trigger={!selected ? ['click'] : []} 1158 1161 menu={{ 1159 - items: org.mrtQueues 1162 + items: (org.mrtQueues ?? []) 1160 1163 .filter((it) => isAppeal === it.isAppealsQueue) 1161 1164 .filter((it) => it.id !== queueId) 1162 1165 .sort((a, b) => a.name.localeCompare(b.name)) ··· 1208 1211 ); 1209 1212 } 1210 1213 1211 - return ( 1214 + const isNcmecDisabled = 1215 + 'type' in action && 1216 + action.type === BuiltInActionType.EnqueueToNcmec && 1217 + !org.hasNCMECReportingEnabled; 1218 + 1219 + const actionDiv = ( 1212 1220 <div 1213 - className={`self-stretch text-start cursor-pointer text-gray-600 font-semibold p-3 ${ 1214 - selected 1215 - ? 'bg-sky-100 text-sky-600' 1216 - : 'bg-white hover:bg-gray-100' 1221 + className={`self-stretch text-start font-semibold p-3 ${ 1222 + isNcmecDisabled 1223 + ? 'cursor-not-allowed text-gray-400 bg-gray-50' 1224 + : selected 1225 + ? 'cursor-pointer bg-sky-100 text-sky-600' 1226 + : 'cursor-pointer text-gray-600 bg-white hover:bg-gray-100' 1217 1227 }`} 1218 1228 key={key} 1219 1229 onClick={() => { 1230 + if (isNcmecDisabled) { 1231 + return; 1232 + } 1220 1233 if (selected) { 1221 1234 // If the action is a built-in action, then nothing else can 1222 1235 // be selected anyway, so we should deselect everything ··· 1317 1330 > 1318 1331 {label} 1319 1332 </div> 1333 + ); 1334 + return isNcmecDisabled ? ( 1335 + <Tooltip 1336 + key={key} 1337 + title="NCMEC reporting is not enabled for your organization." 1338 + > 1339 + {actionDiv} 1340 + </Tooltip> 1341 + ) : ( 1342 + actionDiv 1320 1343 ); 1321 1344 }) 1322 1345 .flatMap((value, i) => [
+55
client/src/webpages/settings/NCMECSettings.tsx
··· 1 1 import { Button } from '@/coop-ui/Button'; 2 2 import { Input } from '@/coop-ui/Input'; 3 3 import { Label } from '@/coop-ui/Label'; 4 + import { 5 + Select, 6 + SelectContent, 7 + SelectItem, 8 + SelectTrigger, 9 + SelectValue, 10 + } from '@/coop-ui/Select'; 4 11 import { toast } from '@/coop-ui/Toast'; 5 12 import { Heading, Text } from '@/coop-ui/Typography'; 6 13 import { ··· 24 31 legalUrl 25 32 ncmecPreservationEndpoint 26 33 ncmecAdditionalInfoEndpoint 34 + defaultNcmecQueueId 27 35 } 28 36 myOrg { 29 37 hasNCMECReportingEnabled 38 + mrtQueues { 39 + id 40 + name 41 + } 30 42 } 31 43 } 32 44 ··· 46 58 legalUrl: string; 47 59 ncmecPreservationEndpoint: string; 48 60 ncmecAdditionalInfoEndpoint: string; 61 + defaultNcmecQueueId: string; 49 62 }; 50 63 51 64 export default function NCMECSettings() { ··· 58 71 legalUrl: '', 59 72 ncmecPreservationEndpoint: '', 60 73 ncmecAdditionalInfoEndpoint: '', 74 + defaultNcmecQueueId: '', 61 75 }); 62 76 63 77 const { loading, error, data } = useGQLNcmecOrgSettingsQuery(); ··· 87 101 data.ncmecOrgSettings.ncmecPreservationEndpoint ?? '', 88 102 ncmecAdditionalInfoEndpoint: 89 103 data.ncmecOrgSettings.ncmecAdditionalInfoEndpoint ?? '', 104 + defaultNcmecQueueId: 105 + data.ncmecOrgSettings.defaultNcmecQueueId ?? '', 90 106 }); 91 107 } 92 108 }, [data?.ncmecOrgSettings]); ··· 126 142 settings.ncmecPreservationEndpoint || null, 127 143 ncmecAdditionalInfoEndpoint: 128 144 settings.ncmecAdditionalInfoEndpoint || null, 145 + defaultNcmecQueueId: settings.defaultNcmecQueueId || null, 129 146 }, 130 147 }, 131 148 }); ··· 265 282 } 266 283 placeholder="https://yourcompany.com/ncmec-info" 267 284 /> 285 + </div> 286 + 287 + <div className="flex flex-col gap-2"> 288 + <Label 289 + htmlFor="defaultNcmecQueueId" 290 + className="text-sm font-medium" 291 + > 292 + Default NCMEC queue 293 + </Label> 294 + <Select 295 + value={settings.defaultNcmecQueueId || '__default__'} 296 + onValueChange={(value) => 297 + setSettings({ 298 + ...settings, 299 + defaultNcmecQueueId: 300 + value === '__default__' ? '' : value, 301 + }) 302 + } 303 + > 304 + <SelectTrigger id="defaultNcmecQueueId"> 305 + <SelectValue placeholder="Use org default queue" /> 306 + </SelectTrigger> 307 + <SelectContent> 308 + <SelectItem value="__default__"> 309 + Use org default queue 310 + </SelectItem> 311 + {(data?.myOrg?.mrtQueues ?? []).map((queue) => ( 312 + <SelectItem key={queue.id} value={queue.id}> 313 + {queue.name} 314 + </SelectItem> 315 + ))} 316 + </SelectContent> 317 + </Select> 318 + <Text size="XS" className="text-gray-500"> 319 + When reviewers choose &quot;Enqueue to NCMEC&quot;, jobs will be 320 + sent to this queue. Leave as &quot;Use org default queue&quot; to 321 + use the organization&apos;s default manual review queue. 322 + </Text> 268 323 </div> 269 324 270 325 <div className="flex flex-col gap-2">
+7
server/graphql/generated.ts
··· 2783 2783 readonly __typename?: 'NcmecOrgSettings'; 2784 2784 readonly companyTemplate?: Maybe<Scalars['String']>; 2785 2785 readonly contactEmail?: Maybe<Scalars['String']>; 2786 + readonly defaultNcmecQueueId?: Maybe<Scalars['String']>; 2786 2787 readonly legalUrl?: Maybe<Scalars['String']>; 2787 2788 readonly moreInfoUrl?: Maybe<Scalars['String']>; 2788 2789 readonly ncmecAdditionalInfoEndpoint?: Maybe<Scalars['String']>; ··· 2794 2795 export type GQLNcmecOrgSettingsInput = { 2795 2796 readonly companyTemplate?: InputMaybe<Scalars['String']>; 2796 2797 readonly contactEmail?: InputMaybe<Scalars['String']>; 2798 + readonly defaultNcmecQueueId?: InputMaybe<Scalars['String']>; 2797 2799 readonly legalUrl?: InputMaybe<Scalars['String']>; 2798 2800 readonly moreInfoUrl?: InputMaybe<Scalars['String']>; 2799 2801 readonly ncmecAdditionalInfoEndpoint?: InputMaybe<Scalars['String']>; ··· 10504 10506 ContextType 10505 10507 >; 10506 10508 contactEmail?: Resolver< 10509 + Maybe<GQLResolversTypes['String']>, 10510 + ParentType, 10511 + ContextType 10512 + >; 10513 + defaultNcmecQueueId?: Resolver< 10507 10514 Maybe<GQLResolversTypes['String']>, 10508 10515 ParentType, 10509 10516 ContextType
+5 -3
server/graphql/modules/manualReviewTool.ts
··· 980 980 case 'USER': { 981 981 return 'appealId' in it 982 982 ? 'UserAppealManualReviewJobPayload' 983 - : 'allMediaItems' in it 984 - ? 'NcmecManualReviewJobPayload' 985 - : 'UserManualReviewJobPayload'; 983 + : it.kind === 'DEFAULT' 984 + ? 'UserManualReviewJobPayload' 985 + : 'allMediaItems' in it 986 + ? 'NcmecManualReviewJobPayload' 987 + : 'UserManualReviewJobPayload'; 986 988 } 987 989 case 'THREAD': { 988 990 return 'appealId' in it
+3
server/graphql/modules/ncmec.ts
··· 28 28 legalUrl: String 29 29 ncmecPreservationEndpoint: String 30 30 ncmecAdditionalInfoEndpoint: String 31 + defaultNcmecQueueId: String 31 32 } 32 33 33 34 input NcmecOrgSettingsInput { ··· 39 40 legalUrl: String 40 41 ncmecPreservationEndpoint: String 41 42 ncmecAdditionalInfoEndpoint: String 43 + defaultNcmecQueueId: String 42 44 } 43 45 44 46 type UpdateNcmecOrgSettingsResponse { ··· 233 235 legalUrl: input.legalUrl ?? null, 234 236 ncmecPreservationEndpoint: input.ncmecPreservationEndpoint ?? null, 235 237 ncmecAdditionalInfoEndpoint: input.ncmecAdditionalInfoEndpoint ?? null, 238 + defaultNcmecQueueId: input.defaultNcmecQueueId ?? null, 236 239 }); 237 240 238 241 return { success: true };
+18 -13
server/iocContainer/index.ts
··· 1413 1413 }); 1414 1414 } 1415 1415 break; 1416 - case 'TRANSFORM_JOB_AND_RECREATE_IN_QUEUE': 1416 + case 'TRANSFORM_JOB_AND_RECREATE_IN_QUEUE': { 1417 + const reportHistory = 1418 + 'reportHistory' in job.payload 1419 + ? job.payload.reportHistory 1420 + : []; 1421 + const reportedForReasons = 1422 + 'reportedForReasons' in job.payload && 1423 + job.payload.reportedForReasons != null && 1424 + job.payload.reportedForReasons.length > 0 1425 + ? job.payload.reportedForReasons 1426 + : reportHistory.map((entry) => ({ 1427 + reporterId: entry.reporterId, 1428 + reason: entry.reason, 1429 + })); 1417 1430 const defaultJobInput = { 1418 1431 enqueueSource: 'MRT_JOB', 1419 1432 enqueueSourceInfo: { kind: 'MRT_JOB' }, 1420 1433 reenqueuedFrom: { jobId: job.id }, 1421 1434 payload: { 1422 - kind: 'DEFAULT', 1435 + kind: 'DEFAULT' as const, 1423 1436 item: job.payload.item, 1424 1437 ...{ 1425 1438 reportIds: ··· 1435 1448 reporterIdentifier: job.payload.reporterIdentifier, 1436 1449 } 1437 1450 : {}), 1438 - ...('reportedForReasons' in job.payload 1439 - ? { 1440 - reportedForReasons: 1441 - job.payload.reportedForReasons ?? [], 1442 - } 1443 - : { reportedForReasons: [] }), 1444 - ...('reportHistory' in job.payload 1445 - ? { 1446 - reportHistory: job.payload.reportHistory, 1447 - } 1448 - : { reportHistory: [] }), 1451 + reportedForReasons, 1452 + reportHistory, 1449 1453 }, 1450 1454 createdAt: new Date(), 1451 1455 orgId, ··· 1480 1484 assertUnreachable(decision.newJobKind); 1481 1485 } 1482 1486 break; 1487 + } 1483 1488 1484 1489 default: 1485 1490 assertUnreachable(decision);
+79
server/services/manualReviewToolService/modules/JobRouting.test.ts
··· 432 432 ); 433 433 434 434 jobRoutingTestWithFixtures( 435 + 'When queueId is passed to enqueue, job is added to that queue (skips routing)', 436 + async ({ 437 + manualReviewToolService, 438 + org, 439 + itemType, 440 + defaultQueue, 441 + anotherQueue, 442 + }) => { 443 + const initialDefault = await manualReviewToolService.getPendingJobCount({ 444 + orgId: org.id, 445 + queueId: defaultQueue.id, 446 + }); 447 + const initialAnother = 448 + await manualReviewToolService.getPendingJobCount({ 449 + orgId: org.id, 450 + queueId: anotherQueue.id, 451 + }); 452 + 453 + const normalizedDataOrError = toNormalizedItemDataOrErrors( 454 + [itemType.id], 455 + itemType, 456 + { text: 'other' }, 457 + ); 458 + if (Array.isArray(normalizedDataOrError)) { 459 + throw new Error('Error validating item data'); 460 + } 461 + 462 + const itemSubmission = await submissionDataToItemSubmission( 463 + async () => itemType, 464 + { 465 + orgId: org.id, 466 + submissionId: makeSubmissionId(), 467 + itemId: uid(), 468 + itemTypeId: itemType.id, 469 + itemTypeVersion: '', 470 + itemTypeSchemaVariant: 'original', 471 + data: normalizedDataOrError, 472 + creatorId: null, 473 + creatorTypeId: null, 474 + }, 475 + ); 476 + if (itemSubmission instanceof Error) { 477 + throw new Error('Error creating item submission'); 478 + } 479 + 480 + const item = 481 + itemSubmissionToItemSubmissionWithTypeIdentifier(itemSubmission); 482 + // Pass explicit queueId so routing is skipped (e.g. NCMEC default queue). 483 + await manualReviewToolService.enqueue( 484 + { 485 + enqueueSource: 'RULE_EXECUTION', 486 + enqueueSourceInfo: { kind: 'RULE_EXECUTION', rules: ['abc'] }, 487 + createdAt: new Date(), 488 + orgId: org.id, 489 + correlationId: toCorrelationId({ type: 'submit-report', id: uid() }), 490 + policyIds: [], 491 + payload: { 492 + kind: 'DEFAULT', 493 + reportHistory: [], 494 + item, 495 + }, 496 + }, 497 + anotherQueue.id, 498 + ); 499 + 500 + const defaultQueueCount = await manualReviewToolService.getPendingJobCount( 501 + { orgId: org.id, queueId: defaultQueue.id }, 502 + ); 503 + const anotherQueueCount = 504 + await manualReviewToolService.getPendingJobCount({ 505 + orgId: org.id, 506 + queueId: anotherQueue.id, 507 + }); 508 + expect(defaultQueueCount).toBe(initialDefault); 509 + expect(anotherQueueCount).toBe(initialAnother + 1); 510 + }, 511 + ); 512 + 513 + jobRoutingTestWithFixtures( 435 514 'Should match on source type', 436 515 async ({ manualReviewToolService, org, itemType, anotherQueue }) => { 437 516 const normalizedDataOrError = toNormalizedItemDataOrErrors(
+28 -2
server/services/manualReviewToolService/modules/QueueOperations.ts
··· 956 956 } 957 957 958 958 /** 959 + * When returning a job that's already in the new format, ensure DEFAULT 960 + * payloads never carry allMediaItems so they resolve to the regular manual 961 + * review view (not NCMEC) and show full decision options. 962 + */ 963 + #returnJobWithNormalizedDefaultPayloadIfNeeded( 964 + job: Job<ManualReviewJob>, 965 + ): Job<ManualReviewJob> { 966 + const p = job.data.payload as Record<string, unknown> & { kind?: string }; 967 + if (p.kind !== 'DEFAULT' || !('allMediaItems' in p)) { 968 + return job; 969 + } 970 + const payloadWithUnknownKeys = job.data.payload as Record<string, unknown>; 971 + const { allMediaItems: _omitted, ...payloadWithoutNcmec } = 972 + payloadWithUnknownKeys; 973 + job.data = { 974 + ...job.data, 975 + payload: payloadWithoutNcmec as ManualReviewJobPayload, 976 + }; 977 + return job; 978 + } 979 + 980 + /** 959 981 * TODO: remove when we no longer need to support legacy jobs 960 982 */ 961 983 async legacyJobToJob( ··· 968 990 'reportedForReasons' in job.data.payload && 969 991 'reportHistory' in job.data.payload 970 992 ) { 971 - return job as Job<ManualReviewJob>; 993 + return this.#returnJobWithNormalizedDefaultPayloadIfNeeded( 994 + job as Job<ManualReviewJob>, 995 + ); 972 996 } 973 997 974 998 const legacyItem = job.data.payload.item; ··· 984 1008 // putting the payload kind in a variable to help TS do some type narrowing. 985 1009 const jobKind = job.data.payload.kind; 986 1010 if (jobKind === 'DEFAULT') { 1011 + const { allMediaItems: _omitted, ...storedPayloadWithoutNcmec } = 1012 + job.data.payload as Record<string, unknown> & { allMediaItems?: unknown }; 987 1013 payload = { 988 - ...job.data.payload, 1014 + ...storedPayloadWithoutNcmec, 989 1015 kind: 'DEFAULT', 990 1016 item: convertedItem, 991 1017
+1
server/services/ncmecService/dbTypes.ts
··· 18 18 legal_url?: string; 19 19 ncmec_preservation_endpoint?: string; 20 20 ncmec_additional_info_endpoint?: string; 21 + default_ncmec_queue_id?: string | null; 21 22 created_at: GeneratedAlways<Date>; 22 23 updated_at: GeneratedAlways<Date>; 23 24 } & (
+28 -23
server/services/ncmecService/ncmecEnqueueToMrt.ts
··· 57 57 item: ItemSubmissionWithTypeIdentifier; 58 58 correlationId: RuleExecutionCorrelationId | ActionExecutionCorrelationId; 59 59 reenqueuedFrom?: OriginJobInfo; 60 + /** When set, NCMEC jobs are enqueued to this queue instead of the org default. */ 61 + targetQueueId?: string; 60 62 } & ( 61 63 | { 62 64 enqueueSource: 'RULE_EXECUTION'; ··· 181 183 } 182 184 183 185 // TODO: Write this to a snowflake table and enqueue based off of a job instead 184 - await this.manualReviewToolService.enqueue({ 185 - orgId, 186 - createdAt, 187 - // TODO: Pass policies through NcmecService.eventuallyEnqueue into 188 - // snowflake and ultimately into here. Note that 189 - // NcmecService.eventuallyEnqueue is called inside submitReport, which has 190 - // access to the policies passed in with the report, and inside the 191 - // ActionPublisher, which has the policies that come along with the 192 - // action. This should be sufficient to pass the policies through to here. 193 - policyIds: [], 194 - payload: { 195 - kind: 'NCMEC', 196 - item: itemSubmissionToItemSubmissionWithTypeIdentifier(userSubmission), 197 - allMediaItems, 198 - reportHistory: [], 186 + await this.manualReviewToolService.enqueue( 187 + { 188 + orgId, 189 + createdAt, 190 + // TODO: Pass policies through NcmecService.eventuallyEnqueue into 191 + // snowflake and ultimately into here. Note that 192 + // NcmecService.eventuallyEnqueue is called inside submitReport, which has 193 + // access to the policies passed in with the report, and inside the 194 + // ActionPublisher, which has the policies that come along with the 195 + // action. This should be sufficient to pass the policies through to here. 196 + policyIds: [], 197 + payload: { 198 + kind: 'NCMEC', 199 + item: itemSubmissionToItemSubmissionWithTypeIdentifier(userSubmission), 200 + allMediaItems, 201 + reportHistory: [], 202 + }, 203 + correlationId: input.correlationId, 204 + // Safe pick to preserve correlation 205 + ...safePick(input, [ 206 + 'enqueueSource', 207 + 'enqueueSourceInfo', 208 + 'reenqueuedFrom', 209 + ]), 199 210 }, 200 - correlationId: input.correlationId, 201 - // Safe pick to preserve correlation 202 - ...safePick(input, [ 203 - 'enqueueSource', 204 - 'enqueueSourceInfo', 205 - 'reenqueuedFrom', 206 - ]), 207 - }); 211 + input.targetQueueId, 212 + ); 208 213 // eslint-disable-next-line no-console 209 214 console.log('[NCMEC] ✅ Successfully created NCMEC manual review job!'); 210 215 return { status: 'ENQUEUED' };
+10 -1
server/services/ncmecService/ncmecService.ts
··· 123 123 } 124 124 ), 125 125 ) { 126 - return this.ncmecEnqueueToMrt.enqueueForHumanReviewIfApplicable(input); 126 + const settings = await this.getNcmecOrgSettings(input.orgId); 127 + const targetQueueId = settings?.defaultNcmecQueueId ?? undefined; 128 + return this.ncmecEnqueueToMrt.enqueueForHumanReviewIfApplicable({ 129 + ...input, 130 + targetQueueId, 131 + }); 127 132 } 128 133 129 134 async getNCMECActionsToRunAndPolicies(orgId: string) { ··· 182 187 'legal_url as legalUrl', 183 188 'ncmec_preservation_endpoint as ncmecPreservationEndpoint', 184 189 'ncmec_additional_info_endpoint as ncmecAdditionalInfoEndpoint', 190 + 'default_ncmec_queue_id as defaultNcmecQueueId', 185 191 ]) 186 192 .where('org_id', '=', orgId) 187 193 .executeTakeFirst(); ··· 199 205 legalUrl: string | null; 200 206 ncmecPreservationEndpoint: string | null; 201 207 ncmecAdditionalInfoEndpoint: string | null; 208 + defaultNcmecQueueId: string | null; 202 209 }) { 203 210 await this.pgQuery 204 211 .insertInto('ncmec_reporting.ncmec_org_settings') ··· 214 221 params.ncmecPreservationEndpoint ?? undefined, 215 222 ncmec_additional_info_endpoint: 216 223 params.ncmecAdditionalInfoEndpoint ?? undefined, 224 + default_ncmec_queue_id: params.defaultNcmecQueueId ?? null, 217 225 actions_to_run_upon_report_creation: null, 218 226 policies_applied_to_actions_run_on_report_creation: null, 219 227 }) ··· 229 237 params.ncmecPreservationEndpoint ?? undefined, 230 238 ncmec_additional_info_endpoint: 231 239 params.ncmecAdditionalInfoEndpoint ?? undefined, 240 + default_ncmec_queue_id: params.defaultNcmecQueueId ?? null, 232 241 }), 233 242 ) 234 243 .execute();