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 253 lines 8.3 kB view raw
1import { type ItemIdentifier } from '@roostorg/types'; 2import _ from 'lodash'; 3import { uid } from 'uid'; 4 5import { type Dependencies } from '../../iocContainer/index.js'; 6import { 7 getUserFromActionTargetItem, 8 type ActionExecutionData, 9 type ActionTargetItem, 10} from '../../rule_engine/ActionPublisher.js'; 11import { 12 itemIdentifierToScyllaItemIdentifier, 13 scyllaItemIdentifierToItemIdentifier, 14 type Scylla, 15} from '../../scylla/index.js'; 16import { type ActionExecutionCorrelationId } from '../analyticsLoggers/ActionExecutionLogger.js'; 17import { filterNullOrUndefined } from '../../utils/collections.js'; 18import { toCorrelationId } from '../../utils/correlationIds.js'; 19import { 20 type Action, 21 type ModerationConfigService, 22} from '../moderationConfigService/index.js'; 23import { type UserStrikesScyllaRelations } from './dbTypes.js'; 24import { type IActionExecutionsAdapter } from '../../plugins/warehouse/queries/IActionExecutionsAdapter.js'; 25 26const { maxBy } = _; 27 28export class UserStrikeService { 29 constructor( 30 private readonly scylla: Scylla<UserStrikesScyllaRelations>, 31 private readonly moderationConfigService: ModerationConfigService, 32 private readonly getUserStrikeTTLinDays: Dependencies['getUserStrikeTTLInDaysEventuallyConsistent'], 33 private readonly actionExecutionsAdapter: IActionExecutionsAdapter, 34 private readonly publishActions: Dependencies['ActionPublisher']['publishActions'], 35 ) { 36 this.scylla = scylla; 37 this.moderationConfigService = moderationConfigService; 38 this.publishActions = publishActions; 39 } 40 41 async getUserStrikes(orgId: string, userId: ItemIdentifier) { 42 return this.scylla.select({ 43 from: 'user_strikes', 44 select: '*', 45 where: [ 46 ['org_id', '=', orgId], 47 ['user_identifier', '=', itemIdentifierToScyllaItemIdentifier(userId)], 48 ], 49 }); 50 } 51 async getUserStrikeValue(orgId: string, userId: ItemIdentifier) { 52 const strikes = await this.scylla.select({ 53 from: 'user_strikes', 54 select: [ 55 'org_id', 56 'user_identifier', 57 { aggregate: 'sum', col: 'user_strike_count' }, 58 ], 59 where: [ 60 ['org_id', '=', orgId], 61 ['user_identifier', '=', itemIdentifierToScyllaItemIdentifier(userId)], 62 ], 63 }); 64 return strikes[0]?.user_strike_count ?? 0; 65 } 66 async getAllUserStrikeCountsForOrg(orgId: string) { 67 const strikes = await this.scylla.select({ 68 from: 'user_strikes', 69 select: [ 70 'user_identifier', 71 { aggregate: 'sum', col: 'user_strike_count' }, 72 ], 73 where: [['org_id', '=', orgId]], 74 groupBy: ['org_id', 'user_identifier'], 75 }); 76 return strikes.map((it) => ({ 77 user_identifier: scyllaItemIdentifierToItemIdentifier(it.user_identifier), 78 strike_count: it.user_strike_count, 79 })); 80 } 81 82 async applyUserStrikeFromPublishedActions< 83 T extends ActionExecutionCorrelationId, 84 U extends ActionTargetItem, 85 >( 86 triggeredActions: Omit< 87 Omit<ActionExecutionData<T>, 'action'> & { action: Action }, 88 'orgId' | 'correlationId' | 'targetItem' 89 >[], 90 executionContext: { 91 orgId: string; 92 correlationId: T; 93 targetItem: U; 94 sync?: boolean; 95 actorId?: string; 96 actorEmail?: string; 97 }, 98 ) { 99 const targetUser = getUserFromActionTargetItem(executionContext.targetItem); 100 const mostSeverePolicy = 101 this.findMostSeverePolicyViolationFromActions(triggeredActions); 102 103 if ( 104 mostSeverePolicy === undefined || 105 mostSeverePolicy.userStrikeCount === 0 || 106 targetUser == null 107 ) { 108 return; 109 } 110 // this variable is not really necessary, but helps 111 // TS keep track of the properly narrowed type of 112 // `mostSeverePolicy.userStrikeCount`, which can only be a number by this point 113 const currentStrikesToApply = mostSeverePolicy.userStrikeCount; 114 115 // we should get the user's current violation count 116 // before applying the strike to avoid double counting 117 const currentUserStrikes = await this.getUserStrikeValue( 118 executionContext.orgId, 119 targetUser, 120 ); 121 122 await this.applyUserStrike( 123 executionContext.orgId, 124 targetUser, 125 mostSeverePolicy.id, 126 mostSeverePolicy.userStrikeCount, 127 ); 128 129 // find threshold rules 130 const orgThresholds = 131 await this.moderationConfigService.getUserStrikeThresholdsForOrg( 132 executionContext.orgId, 133 ); 134 135 if (orgThresholds.length === 0) { 136 return; 137 } 138 // To find which, if any, threshold rules to apply to this user, we 139 // 1. find all threshold rules that specify a threshold that is greater 140 // than the user's current strike count (meaning this current strike 141 // application could trigger the threshold rule) 142 // 2. find all the thresholds that have been crossed by applying the current 143 // strike to the user 144 // 3. find the greatest/most severe threshold rule that has been crossed. 145 // i.e. we will not apply multiple threshold rules if multiple are 146 // crossed at once, only the last/most severe one 147 const thresholdRuleToApply = orgThresholds 148 .filter((it) => it.threshold > currentUserStrikes) 149 .filter( 150 (it) => currentUserStrikes + currentStrikesToApply >= it.threshold, 151 ) 152 // sort by threshold in descending order 153 .sort((a, b) => b.threshold - a.threshold)[0]; 154 155 // construst the actions to publish 156 const actionsToPublish = await this.moderationConfigService.getActions({ 157 orgId: executionContext.orgId, 158 ids: thresholdRuleToApply.actions, 159 readFromReplica: true, 160 }); 161 const actionExecutionDataArray = actionsToPublish.map((action) => ({ 162 action, 163 orgId: executionContext.orgId, 164 targetItem: executionContext.targetItem, 165 policies: [], 166 matchingRules: undefined, 167 ruleEnvironment: undefined, 168 })); 169 await this.publishActions(actionExecutionDataArray, { 170 orgId: executionContext.orgId, 171 correlationId: toCorrelationId({ 172 type: 'user-strike-action-execution', 173 id: uid(), 174 }), 175 targetItem: executionContext.targetItem, 176 actorId: undefined, 177 actorEmail: undefined, 178 }); 179 } 180 181 findMostSeverePolicyViolationFromActions< 182 T extends ActionExecutionCorrelationId, 183 >( 184 triggeredActions: Omit< 185 Omit<ActionExecutionData<T>, 'action'> & { action: Action }, 186 'orgId' | 'correlationId' | 'targetItem' 187 >[], 188 ) { 189 const mostSeverePolicyPerAction = filterNullOrUndefined( 190 triggeredActions.map((action) => { 191 if (action.action.applyUserStrikes === false) { 192 return undefined; 193 } 194 return maxBy(action.policies, (policy) => policy.userStrikeCount); 195 }), 196 ); 197 return maxBy(mostSeverePolicyPerAction, (policy) => policy.userStrikeCount); 198 } 199 200 async applyUserStrike( 201 orgId: string, 202 userId: ItemIdentifier, 203 policyId: string, 204 numStrikes: number, 205 ) { 206 const ttlInDays = await this.getUserStrikeTTLinDays(orgId); 207 208 await this.scylla.insert({ 209 into: 'user_strikes', 210 row: { 211 user_identifier: itemIdentifierToScyllaItemIdentifier(userId), 212 created_at: new Date(), 213 policy_id: policyId, 214 org_id: orgId, 215 user_strike_count: numStrikes, 216 }, 217 // The table has a default TTL of 90 Days, which we will usually override 218 // with the org's configured TTL. In the case we don't find one, we can 219 // just let the database use the TTL instead of specifying a default in the 220 // insert statement. 221 ttlInSeconds: ttlInDays ? ttlInDays * 24 * 60 * 60 : undefined, 222 }); 223 } 224 225 async getRecentUserStrikeActions(opts: { 226 orgId: string; 227 filterBy?: { 228 startDate?: Date; 229 endDate?: Date; 230 }; 231 limit?: number; 232 }) { 233 const { orgId, filterBy, limit } = opts; 234 const results = await this.actionExecutionsAdapter.getRecentUserStrikeActions({ 235 orgId, 236 filterBy, 237 limit, 238 }); 239 return filterNullOrUndefined( 240 results.map((it) => 241 it.itemId && it.itemTypeId 242 ? { 243 actionId: it.actionId, 244 itemId: it.itemId, 245 itemTypeId: it.itemTypeId, 246 source: it.source, 247 time: it.occurredAt, 248 } 249 : undefined, 250 ), 251 ); 252 } 253}