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 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 182 lines 5.9 kB view raw
1import sequelize, { 2 type CreationOptional, 3 type HasManyGetAssociationsMixin, 4 type InferAttributes, 5 type InferCreationAttributes, 6 type Sequelize, 7} from 'sequelize'; 8 9import { UserPenaltySeverity } from '../services/moderationConfigService/index.js'; 10import { validateUrl } from '../utils/url.js'; 11import { type LocationBank } from './banks/LocationBankModel.js'; 12import { type DataTypes } from './index.js'; 13import { type Policy } from './PolicyModel.js'; 14import { type SequelizeAction } from './rules/ActionModel.js'; 15import { type Rule } from './rules/RuleModel.js'; 16import { type User } from './UserModel.js'; 17 18const { Model } = sequelize; 19 20export type Org = InstanceType<ReturnType<typeof makeOrgModel>>; 21export type PolicyActionPenalties = { 22 actionId: string; 23 policyId: string; 24 penalties: number[]; 25}; 26 27/** 28 * Data Model for Organizations 29 */ 30export default function makeOrgModel( 31 sequelize: Sequelize, 32 DataTypes: DataTypes, 33) { 34 class Org extends Model< 35 InferAttributes<Org, { omit: 'createdAt' | 'updatedAt' }>, 36 InferCreationAttributes<Org, { omit: 'createdAt' | 'updatedAt' }> 37 > { 38 public declare id: string; 39 public declare email: string; 40 public declare name: string; 41 public declare websiteUrl: string; 42 public declare apiKeyId?: CreationOptional<string>; 43 public declare onCallAlertEmail?: CreationOptional<string>; 44 45 public declare getRules: HasManyGetAssociationsMixin<Rule>; 46 public declare getActions: HasManyGetAssociationsMixin<SequelizeAction>; 47 // Has to use any below to avoid circular type errors. 48 // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 public declare getContentTypes: HasManyGetAssociationsMixin<any>; 50 public declare getLocationBanks: HasManyGetAssociationsMixin<LocationBank>; 51 public declare getUsers: HasManyGetAssociationsMixin<User>; 52 public declare getPolicies: HasManyGetAssociationsMixin<Policy>; 53 public declare createdAt: Date; 54 public declare updatedAt: Date; 55 56 // eslint-disable-next-line @typescript-eslint/no-explicit-any 57 static associate(models: { [key: string]: any }) { 58 Org.hasMany(models.User, { as: 'Users' }); 59 Org.hasMany(models.Rule, { as: 'Rules' }); 60 Org.hasMany(models.Action, { as: 'Actions', foreignKey: 'orgId' }); 61 Org.hasMany(models.ItemType, { as: 'ContentTypes' }); 62 Org.hasMany(models.LocationBank, { as: 'LocationBanks' }); 63 Org.hasMany(models.Policy, { as: 'policies' }); 64 } 65 66 static async getPolicyActionPenaltiesEventuallyConsistent(orgId: string) { 67 const [actions, policies] = await Promise.all([ 68 sequelize.models.Action.findAll({ where: { orgId } }), 69 sequelize.models.Policy.findAll({ where: { orgId } }), 70 ]); 71 72 return (policies as Policy[]).flatMap((policy) => 73 (actions as SequelizeAction[]).map( 74 (action): PolicyActionPenalties => ({ 75 actionId: action.id, 76 policyId: policy.id, 77 penalties: [ 78 computeActionPolicyPenalty(action.penalty, policy.penalty), 79 ], 80 }), 81 ), 82 ); 83 } 84 } 85 86 /* Fields */ 87 Org.init( 88 { 89 id: { 90 type: DataTypes.STRING, 91 primaryKey: true, 92 }, 93 email: { 94 type: DataTypes.STRING, 95 unique: true, 96 allowNull: false, 97 validate: { 98 isEmail: true, 99 notEmpty: true, 100 }, 101 }, 102 name: { 103 type: DataTypes.STRING, 104 unique: true, 105 allowNull: false, 106 validate: { 107 notEmpty: true, 108 }, 109 }, 110 websiteUrl: { 111 type: DataTypes.STRING, 112 unique: true, 113 allowNull: false, 114 validate: { 115 isValidUrl: validateUrl, 116 }, 117 }, 118 // ID of the AWS API Key resource that stores the API key. Not actually 119 // used for anything at the moment (instead, the API key is looked up in 120 // but potentially useful. 121 apiKeyId: { 122 type: DataTypes.STRING, 123 }, 124 onCallAlertEmail: { 125 type: DataTypes.STRING, 126 validate: { 127 isEmail: true, 128 }, 129 }, 130 }, 131 { 132 sequelize, 133 modelName: 'org', 134 underscored: true, 135 }, 136 ); 137 138 return Org; 139} 140 141/** 142 * Computes the severity of the penalty we should apply for a given 143 * (action, policy) pair. The general idea is to make the penalties 144 * increase exponentially as severity levels increase, but the rate 145 * of increase can't be so high that a (severe, severe) penalty is 146 * 50x higher than a (high, high) penalty. 147 * 148 * The easiest way to achieve this exponential behavior is at the individual 149 * severity levels, rather than trying to multiply the action penalty 150 * by the severity penalty to compound their magnitudes. So the severity 151 * levels apply penalty magnitudes as follows: 152 * 153 * NONE = 0 154 * LOW = 1 155 * MEDIUM = 3 156 * HIGH = 9 157 * SEVERE = 27 158 * 159 * To get the penalty value for an (action, policy) pair, we just add the 160 * penalty values of the action and penalty because the exponential nature 161 * of these penalties has already been taken into account. 162 */ 163function computeActionPolicyPenalty( 164 actionPenalty: UserPenaltySeverity, 165 policyPenalty: UserPenaltySeverity, 166) { 167 // Type annotation makes sure that every possible severity has a score. 168 const penaltySeverityMap: { [k in UserPenaltySeverity]: number } = { 169 [UserPenaltySeverity.NONE]: 0, 170 [UserPenaltySeverity.LOW]: 1, 171 [UserPenaltySeverity.MEDIUM]: 3, 172 [UserPenaltySeverity.HIGH]: 9, 173 [UserPenaltySeverity.SEVERE]: 27, 174 }; 175 176 // If the action has no penalty (e.g., "Send to Moderation", "Restore 177 // Content"), we never apply any penalty, regardless of the policy penalty. 178 // Otherwise, the penalty accounts for both the action + policy penalties. 179 return actionPenalty === UserPenaltySeverity.NONE 180 ? 0 181 : penaltySeverityMap[actionPenalty] + penaltySeverityMap[policyPenalty]; 182}