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

Configure Feed

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

at main 500 lines 14 kB view raw
1import { type Kysely } from 'kysely'; 2import _ from 'lodash'; 3import { type JsonObject, type ReadonlyDeep } from 'type-fest'; 4 5import { type ConsumerDirectives } from '../../lib/cache/index.js'; 6import type { Invoker } from '../../models/types/permissioning.js'; 7import { type RuleErrorType, type LocationBankErrorType } from './errors.js'; 8import { type ModerationConfigServicePg } from './dbTypes.js'; 9import { type Action, type CustomAction, type Policy } from './index.js'; 10import ActionOperations, { 11 type ActionErrorType, 12} from './modules/ActionOperations.js'; 13import ItemTypeOperations from './modules/ItemTypeOperations.js'; 14import MatchingBankOperations, { 15 type MatchingBankErrorType, 16} from './modules/MatchingBankOperations.js'; 17import PolicyOperations, { 18 type PolicyErrorType, 19} from './modules/PolicyOperations.js'; 20import RuleReadOperations from './modules/RuleReadOperations.js'; 21import UserStrikeOperations, { 22 type UserStrikeThresholdErrorType, 23} from './modules/UserStrikeOperations.js'; 24import { 25 type ContentItemType, 26 type ItemSchema, 27 type ItemType, 28 type ItemTypeKind, 29 type ItemTypeSelector, 30 type ThreadItemType, 31 type UserItemType, 32} from './types/itemTypes.js'; 33import type { PolicyType } from './types/policies.js'; 34import { type PlainRuleWithLatestVersion } from '../../models/rules/ruleTypes.js'; 35 36export type ModerationConfigErrorType = 37 | 'AttemptingToDeleteDefaultUserType' 38 | ActionErrorType 39 | PolicyErrorType 40 | UserStrikeThresholdErrorType 41 | RuleErrorType 42 | LocationBankErrorType 43 | MatchingBankErrorType; 44 45// By having the ModerationConfigService `implement` this type, TS will check 46// for us that every ModerationConfigService method returns one of our public 47// types. 48type ReturnsModerationConfigTypes = { 49 [K in keyof ModerationConfigService]: ReturnType< 50 ModerationConfigService[K] 51 > extends ArrayOrPromiseOf<void | ItemType | Action | Policy | boolean> 52 ? ModerationConfigService[K] 53 : never; 54}; 55 56type ArrayOrPromiseOf<T> = 57 | ReadonlyDeep<T> 58 | readonly ReadonlyDeep<T>[] 59 | Promise<readonly ReadonlyDeep<T>[]> 60 | Promise<ReadonlyDeep<T>>; 61 62type ContentTypeSchemaFieldRoles = { 63 creatorId?: string | null; 64 threadId?: string | null; 65 parentId?: string | null; 66 createdAt?: string | null; 67 displayName?: string | null; 68}; 69 70type ThreadTypeSchemaFieldRoles = { 71 createdAt?: string | null; 72 displayName?: string | null; 73 creatorId?: string | null; 74}; 75 76type UserTypeSchemaFieldRoles = { 77 profileIcon?: string | null; 78 backgroundImage?: string | null; 79 createdAt?: string | null; 80 displayName?: string | null; 81}; 82 83/** 84 * This service will eventually manage all CRUD operations on entities that are 85 * part of an organization's defined moderation policy, including: rules, 86 * actions, policies, item types, and banks — basically, everything in an org 87 * except for the org’s users (which are fairly disconnected). 88 * 89 * The scope of this service is intentionally very broad, and it should not be 90 * sub-divided lightly; see the rationale at 91 * https://coop.atlassian.net/browse/COOP-743?focusedCommentId=10223 92 */ 93export class ModerationConfigService implements ReturnsModerationConfigTypes { 94 private readonly actionOps: ActionOperations; 95 private readonly policyOps: PolicyOperations; 96 private readonly itemTypeOps: ItemTypeOperations; 97 private readonly userStrikeOps: UserStrikeOperations; 98 private readonly matchingBankOps: MatchingBankOperations; 99 private readonly ruleReadOps: RuleReadOperations; 100 101 constructor( 102 pgQuery: Kysely<ModerationConfigServicePg>, 103 pgQueryReplica: Kysely<ModerationConfigServicePg>, 104 private readonly onDeletePolicyId: (opts: { 105 policyId: string; 106 orgId: string; 107 }) => Promise<void>, 108 ) { 109 this.actionOps = new ActionOperations(pgQuery, pgQueryReplica); 110 this.policyOps = new PolicyOperations( 111 pgQuery, 112 pgQueryReplica, 113 onDeletePolicyId, 114 ); 115 this.itemTypeOps = new ItemTypeOperations(pgQuery, pgQueryReplica); 116 this.userStrikeOps = new UserStrikeOperations(pgQuery, pgQueryReplica); 117 this.matchingBankOps = new MatchingBankOperations(pgQuery, pgQueryReplica); 118 this.ruleReadOps = new RuleReadOperations(pgQuery, pgQueryReplica); 119 } 120 121 async getItemTypes(opts: { 122 orgId: string; 123 directives?: ConsumerDirectives; 124 }): Promise<readonly ReadonlyDeep<ItemType>[]> { 125 return this.itemTypeOps.getItemTypes(opts); 126 } 127 128 async getItemType(opts: { 129 orgId: string; 130 itemTypeSelector: ItemTypeSelector; 131 directives?: ConsumerDirectives; 132 }) { 133 return this.itemTypeOps.getItemType(opts); 134 } 135 136 async getItemTypesByKind<T extends ItemTypeKind>(opts: { 137 orgId: string; 138 kind: T; 139 directives?: ConsumerDirectives; 140 }): Promise<readonly ReadonlyDeep<ItemType & { kind: T }>[]> { 141 return this.itemTypeOps.getItemTypesByKind(opts); 142 } 143 144 async getDefaultUserType(opts: { 145 orgId: string; 146 directives?: ConsumerDirectives; 147 }) { 148 return this.itemTypeOps.getDefaultUserType(opts); 149 } 150 151 async createDefaultUserType(orgId: string) { 152 return this.itemTypeOps.createDefaultUserType(orgId); 153 } 154 155 async createContentType( 156 orgId: string, 157 input: { 158 name: string; 159 schema: ItemSchema; 160 description?: string | null; 161 schemaFieldRoles: ContentTypeSchemaFieldRoles; 162 }, 163 ): Promise<ReadonlyDeep<ContentItemType>> { 164 return this.itemTypeOps.createContentType(orgId, input); 165 } 166 167 async updateContentType( 168 orgId: string, 169 input: { 170 id: string; 171 name?: string; 172 schema?: ItemSchema; 173 description?: string | null; 174 schemaFieldRoles: ContentTypeSchemaFieldRoles; 175 }, 176 ): Promise<ReadonlyDeep<ContentItemType>> { 177 return this.itemTypeOps.updateContentType(orgId, input); 178 } 179 180 async createThreadType( 181 orgId: string, 182 input: { 183 name: string; 184 schema: ItemSchema; 185 description?: string | null; 186 schemaFieldRoles: ThreadTypeSchemaFieldRoles; 187 }, 188 ): Promise<ReadonlyDeep<ThreadItemType>> { 189 return this.itemTypeOps.createThreadType(orgId, input); 190 } 191 192 async updateThreadType( 193 orgId: string, 194 input: { 195 id: string; 196 name?: string; 197 schema?: ItemSchema; 198 description?: string | null; 199 schemaFieldRoles: ThreadTypeSchemaFieldRoles; 200 }, 201 ): Promise<ReadonlyDeep<ThreadItemType>> { 202 return this.itemTypeOps.updateThreadType(orgId, input); 203 } 204 205 async createUserType( 206 orgId: string, 207 input: { 208 name: string; 209 schema: ItemSchema; 210 description?: string | null; 211 schemaFieldRoles: UserTypeSchemaFieldRoles; 212 }, 213 ): Promise<ReadonlyDeep<UserItemType>> { 214 return this.itemTypeOps.createUserType(orgId, input); 215 } 216 217 async updateUserType( 218 orgId: string, 219 input: { 220 id: string; 221 name?: string; 222 schema?: ItemSchema; 223 description?: string | null; 224 schemaFieldRoles: UserTypeSchemaFieldRoles; 225 }, 226 ): Promise<ReadonlyDeep<UserItemType>> { 227 return this.itemTypeOps.updateUserType(orgId, input); 228 } 229 230 async deleteItemType(opts: { orgId: string; itemTypeId: string }) { 231 return this.itemTypeOps.deleteItemType(opts); 232 } 233 234 async getItemTypesForAction(opts: { 235 orgId: string; 236 actionId: string; 237 directives?: ConsumerDirectives; 238 }): Promise<ItemType[]> { 239 return this.itemTypeOps.getItemTypesForAction(opts); 240 } 241 242 async getItemTypesForRule(opts: { 243 orgId: string; 244 ruleId: string; 245 readFromReplica?: boolean; 246 }): Promise<ItemType[]> { 247 return this.itemTypeOps.getItemTypesForRule(opts); 248 } 249 250 async createAction( 251 orgId: string, 252 input: { 253 name: string; 254 description: string | null; 255 // TODO: support other types? Need to figure out relationship between 256 // activating various org settings (e.g., to enable MRT or NCMEC reporting) 257 // and this moderationConfigService. 258 type: 'CUSTOM_ACTION'; 259 callbackUrl: string; 260 callbackUrlHeaders: JsonObject | null; 261 callbackUrlBody: JsonObject | null; 262 applyUserStrikes?: boolean; 263 itemTypeIds?: readonly string[]; 264 }, 265 ): Promise<CustomAction> { 266 return this.actionOps.createAction(orgId, input); 267 } 268 269 async updateCustomAction( 270 orgId: string, 271 opts: { 272 actionId: string; 273 patch: { 274 name?: string; 275 description?: string | null; 276 callbackUrl?: string; 277 callbackUrlHeaders?: JsonObject | null; 278 callbackUrlBody?: JsonObject | null; 279 applyUserStrikes?: boolean; 280 }; 281 itemTypeIds?: readonly string[] | undefined; 282 }, 283 ): Promise<CustomAction> { 284 return this.actionOps.updateCustomAction({ orgId, ...opts }); 285 } 286 287 async deleteCustomAction(opts: { orgId: string; actionId: string }) { 288 return this.actionOps.deleteCustomAction(opts); 289 } 290 async upsertBuiltInActions(orgId: string) { 291 return this.actionOps.upsertBuiltInActions(orgId); 292 } 293 294 async getActions(opts: { 295 orgId: string; 296 ids?: readonly string[]; 297 readFromReplica?: boolean; 298 }) { 299 return this.actionOps.getActions(opts); 300 } 301 302 async getActionsForItemType(opts: { 303 orgId: string; 304 itemTypeId: string; 305 itemTypeKind: ItemTypeKind; 306 readFromReplica?: boolean; 307 }) { 308 return this.actionOps.getActionsForItemType(opts); 309 } 310 311 async getActionsForRuleId(opts: { 312 orgId: string; 313 ruleId: string; 314 readFromReplica?: boolean; 315 }) { 316 return this.actionOps.getActionsForRuleId(opts); 317 } 318 319 async getPoliciesByRuleIds(ruleIds: readonly string[]) { 320 return this.policyOps.getPoliciesByRuleIds({ 321 ruleIds, 322 readFromReplica: true, 323 }); 324 } 325 326 async getEnabledRulesForItemType(itemTypeId: string) { 327 return this.ruleReadOps.getEnabledRulesForItemType(itemTypeId); 328 } 329 330 async getRuleByIdAndOrg( 331 ruleId: string, 332 orgId: string, 333 opts?: { readFromReplica?: boolean }, 334 ): Promise<PlainRuleWithLatestVersion | null> { 335 return this.ruleReadOps.getRuleByIdAndOrg(ruleId, orgId, opts); 336 } 337 338 async getRulesForOrg( 339 orgId: string, 340 opts?: { readFromReplica?: boolean }, 341 ): Promise<readonly PlainRuleWithLatestVersion[]> { 342 return this.ruleReadOps.getRulesForOrg(orgId, opts); 343 } 344 345 async findEnabledUserRules(): Promise<PlainRuleWithLatestVersion[]> { 346 return this.ruleReadOps.findEnabledUserRules(); 347 } 348 349 async getPolicies(opts: { orgId: string; readFromReplica?: boolean }) { 350 return this.policyOps.getPolicies(opts); 351 } 352 353 async getPoliciesByIds(opts: { 354 orgId: string; 355 ids: readonly string[]; 356 readFromReplica?: boolean; 357 }) { 358 return this.policyOps.getPoliciesByIds(opts); 359 } 360 361 async getPolicy(opts: { 362 orgId: string; 363 policyId: string; 364 readFromReplica?: boolean; 365 }) { 366 return this.policyOps.getPolicy(opts); 367 } 368 369 async createPolicy(opts: { 370 orgId: string; 371 policy: { 372 name: string; 373 parentId: string | null; 374 policyText: string | null; 375 enforcementGuidelines: string | null; 376 policyType: PolicyType | null; 377 }; 378 invokedBy: Invoker; 379 }): Promise<Policy> { 380 return this.policyOps.createPolicy(opts); 381 } 382 383 async updatePolicy(opts: { 384 orgId: string; 385 policy: { 386 id: string; 387 name?: string; 388 parentId?: string | null; 389 policyText?: string | null; 390 enforcementGuidelines?: string | null; 391 policyType?: PolicyType | null; 392 userStrikeCount?: number | null; 393 applyUserStrikeCountConfigToChildren?: boolean | null; 394 }; 395 invokedBy: Invoker; 396 }): Promise<Policy> { 397 return this.policyOps.updatePolicy(opts); 398 } 399 400 async deletePolicy(opts: { 401 orgId: string; 402 policyId: string; 403 invokedBy: Invoker; 404 }) { 405 return this.policyOps.deletePolicy(opts); 406 } 407 408 async getUserStrikeThresholdsForOrg(orgId: string) { 409 return this.userStrikeOps.getUserStrikeThresholds({ 410 orgId, 411 readFromReplica: true, 412 }); 413 } 414 415 async createUserStrikeThreshold(opts: { 416 orgId: string; 417 thresholdSettings: { 418 threshold: number; 419 actions: string[]; 420 }; 421 }) { 422 return this.userStrikeOps.createUserStrikeThreshold(opts); 423 } 424 425 async setAllUserStrikeThresholds(opts: { 426 orgId: string; 427 thresholds: readonly { 428 threshold: number; 429 actions: readonly string[]; 430 }[]; 431 }) { 432 return this.userStrikeOps.setAllUserStrikeThresholds(opts); 433 } 434 435 async updateUserStrikeThreshold(opts: { 436 orgId: string; 437 thresholdSettings: { id: string; threshold?: number; actions?: string[] }; 438 }) { 439 return this.userStrikeOps.updateUserStrikeThreshold(opts); 440 } 441 442 async deleteUserStrikeThreshold(opts: { 443 orgId: string; 444 445 thresholdSettings: { id: string; threshold: number }; 446 }) { 447 return this.userStrikeOps.deleteUserStrikeThreshold({ 448 orgId: opts.orgId, 449 id: opts.thresholdSettings.id, 450 threshold: opts.thresholdSettings.threshold, 451 }); 452 } 453 454 async getTextBank(opts: { 455 orgId: string; 456 id: string; 457 readFromReplica?: boolean; 458 }) { 459 return this.matchingBankOps.getTextBank(opts); 460 } 461 462 async getTextBanks(opts: { orgId: string; readFromReplica?: boolean }) { 463 return this.matchingBankOps.getTextBanks(opts); 464 } 465 466 async createTextBank( 467 orgId: string, 468 input: { 469 name: string; 470 description: string | null; 471 type: 'STRING' | 'REGEX'; 472 ownerId?: string | null; 473 strings: string[]; 474 }, 475 ) { 476 return this.matchingBankOps.createTextBank(orgId, input); 477 } 478 479 async updateTextBank( 480 orgId: string, 481 input: { 482 id: string; 483 name?: string; 484 description?: string | null; 485 type?: 'STRING' | 'REGEX'; 486 ownerId?: string | null; 487 strings?: string[]; 488 }, 489 ) { 490 return this.matchingBankOps.updateTextBank(orgId, input); 491 } 492 493 async deleteTextBank(orgId: string, id: string) { 494 return this.matchingBankOps.deleteTextBank(orgId, id); 495 } 496 async close() { 497 await this.itemTypeOps.close(); 498 } 499} 500