-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #216 from hymccord/hunt-rejection-feature
Add hunt rejection engine
- Loading branch information
Showing
13 changed files
with
610 additions
and
64 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { HgResponse, User } from "../types/hg"; | ||
import { IntakeMessage } from "../types/mhct"; | ||
import { LoggerService } from "../util/logger"; | ||
import { IRule, IPropertyRule, IMessageExemption } from "./interfaces"; | ||
import { ResponseRules } from "./responseRules"; | ||
import { UserRules } from "./userRules"; | ||
import { MessageRules } from "./messageRules"; | ||
import { MessageExemptions } from "./messageExemptions"; | ||
|
||
/** | ||
* Uses pluggable rule to validate data before a hunt can be | ||
* submitted to the database | ||
*/ | ||
export class IntakeRejectionEngine { | ||
private logger: LoggerService; | ||
private responseRules: IRule<HgResponse>[] = []; | ||
private userRules: IRule<User>[] = []; | ||
private messageRules: IPropertyRule<IntakeMessage>[] = []; | ||
private messageExemptions: IMessageExemption[] = []; | ||
|
||
constructor(logger: LoggerService) { | ||
this.logger = logger; | ||
this.initResponseRules(); | ||
this.initUserRules(); | ||
this.initMessageRules(); | ||
}; | ||
|
||
public validateResponse(pre: HgResponse, post: HgResponse): boolean { | ||
return this.responseRules.every(r => { | ||
const isValid = r.isValid(pre, post); | ||
if (!isValid) { | ||
this.logger.debug(`Api responses invalid: ${r.description}`); | ||
} | ||
return isValid; | ||
}); | ||
} | ||
|
||
public validateUser(pre: User, post: User): boolean { | ||
return this.userRules.every(r => { | ||
let isValid = r.isValid(pre, post); | ||
if (!isValid) { | ||
this.logger.debug(`User objects invalid: ${r.description}`); | ||
} | ||
return isValid; | ||
}); | ||
} | ||
|
||
public validateMessage(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
// TODO: Refactor? Location is being set to null in stage funcs so it'll fail later | ||
if (pre.location === null || post.location === null) { | ||
return false; | ||
} | ||
|
||
// Run rules. Build set of currently invalid properties | ||
// { "location", "stage" } | ||
const invalidProperties = new Set<(keyof IntakeMessage)>(); | ||
for (const rule of this.messageRules) { | ||
const valid: boolean = rule.isValid(pre, post); | ||
if (!valid) { | ||
this.logger.debug(`Message invalid: ${rule.description}`); | ||
invalidProperties.add(rule.property); | ||
} | ||
} | ||
|
||
// Don't have to run exemption rules if there are no violations | ||
if (invalidProperties.size === 0) { | ||
return true; | ||
} | ||
|
||
// Find exception objects that will run for given key | ||
const exemption_providers = this.messageExemptions.filter(e => invalidProperties.has(e.property)); | ||
this.logger.debug(`Got ${exemption_providers.length} exemption providers for these invalid properties:`, ...invalidProperties.values()); | ||
|
||
// Do we need to filter them furthur? | ||
// Exception object could give specific key and value to filter on. | ||
// Didn't pre-optimize but could be worth if there are many, many exemptions and they are slow (doubtful) | ||
for (const provider of exemption_providers) { | ||
// Each exemption can give multiple keys that it accounts for. | ||
// For example, the location and stage will change when catching realm ripper | ||
// so that exemption provides [ "location", "stage" ] | ||
const exemptions = provider.getExemptions(pre, post); | ||
if (exemptions && exemptions.length > 0) { | ||
this.logger.debug(`Got exemptions. Description: ${provider.description}`, { properties: exemptions }); | ||
exemptions.forEach(e => invalidProperties.delete(e)); | ||
} | ||
|
||
if (invalidProperties.size == 0) { | ||
this.logger.debug('Message was revalidated due to exemptions.'); | ||
return true; | ||
} | ||
} | ||
|
||
this.logger.debug(`Message object invalid`, { | ||
properties: { ...invalidProperties.values() }, | ||
messages: { pre, post } | ||
}) | ||
|
||
return false; | ||
} | ||
|
||
private initResponseRules() { | ||
this.responseRules = ResponseRules; | ||
} | ||
|
||
private initUserRules() { | ||
this.userRules = UserRules; | ||
} | ||
|
||
private initMessageRules() { | ||
this.messageRules = MessageRules; | ||
this.messageExemptions = MessageExemptions | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { IntakeMessage } from "../types/mhct"; | ||
|
||
/** | ||
* Describes a way to validate a pre and post object | ||
*/ | ||
export interface IRule<K> { | ||
/** | ||
* Short statement explaining rule. | ||
*/ | ||
readonly description: string; | ||
/** | ||
* Check if two objects are valid between each other. | ||
* @param pre The pre-hunt object | ||
* @param post The post-hunt object | ||
* @returns true if valid, otherwise false | ||
*/ | ||
isValid(pre: K, post: K): boolean; | ||
} | ||
|
||
/** | ||
* Provides a rule for a specific property of T | ||
*/ | ||
export interface IPropertyRule<T> extends IRule<T> { | ||
readonly property: (keyof T); | ||
} | ||
|
||
/** | ||
* Contract for implementing exemptions of IntakeMessage | ||
*/ | ||
export interface IMessageExemption { | ||
/** | ||
* Short statement explaining exemption. | ||
*/ | ||
readonly description: string; | ||
/** | ||
* When this property name is invalid on an intake message, | ||
* this rule will try to provide exemptions. | ||
*/ | ||
readonly property: (keyof IntakeMessage); | ||
/** | ||
* Get exemptions for the given pre and post messages | ||
* @param pre The pre-hunt IntakeMessage | ||
* @param post The post-hunt IntakeMessage | ||
* @returns An array of keys (of IntakeMessage) for which properties has been exempted or null. | ||
*/ | ||
getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { IntakeMessage } from "../types/mhct"; | ||
import { IMessageExemption } from "./interfaces"; | ||
|
||
// Acolyte Realm | ||
|
||
/** | ||
* Provides an exemption on the 'location' difference. Iff the mouse was | ||
* a Realm Ripper and the user moved from FG -> AR. Give exemptions for | ||
* 'location' and 'stage'. | ||
*/ | ||
class RealmRipperLocationExemption implements IMessageExemption { | ||
readonly description = "Realm Ripper caught in Forbidden Grove"; | ||
readonly property = "location"; | ||
getExemptions(pre: IntakeMessage, post: IntakeMessage): (keyof IntakeMessage)[] | null { | ||
if (pre.location?.name === "Forbidden Grove" | ||
&& post.location?.name === "Acolyte Realm" | ||
&& pre.mouse === "Realm Ripper") { | ||
return [ "location", "stage" ] | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
|
||
export const MessageExemptions: IMessageExemption[] = [ | ||
new RealmRipperLocationExemption, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { IntakeMessage } from "../types/mhct"; | ||
import { IPropertyRule } from "./interfaces"; | ||
|
||
class IntakeMessageSameCheese implements IPropertyRule<IntakeMessage> { | ||
readonly description = "Cheese should not change"; | ||
readonly property = "cheese"; | ||
isValid(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
return pre.cheese.name === post.cheese.name; | ||
} | ||
} | ||
|
||
class IntakeMessageSameWeapon implements IPropertyRule<IntakeMessage> { | ||
readonly description = "Trap should not change"; | ||
readonly property = "trap"; | ||
isValid(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
return pre.trap.name === post.trap.name; | ||
} | ||
} | ||
|
||
class IntakeMessageSameBase implements IPropertyRule<IntakeMessage> { | ||
readonly description = "Base should not change"; | ||
readonly property = "base"; | ||
isValid(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
return pre.base.name === post.base.name; | ||
} | ||
} | ||
|
||
class IntakeMessageSameLocation implements IPropertyRule<IntakeMessage> { | ||
readonly description = "Location should not change"; | ||
readonly property = "location"; | ||
isValid(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
return (pre.location !== null && post.location !== null) | ||
&& pre.location.name === post.location.name; | ||
} | ||
} | ||
|
||
class IntakeMessageSameStage implements IPropertyRule<IntakeMessage> { | ||
readonly description = "Stage should not change"; | ||
readonly property = "stage"; | ||
isValid(pre: IntakeMessage, post: IntakeMessage): boolean { | ||
// Juggling check. Valid if both stages are undefined or null. | ||
if (pre.stage == null && post.stage == null) { | ||
return true; | ||
} | ||
|
||
// stage can be a object so just use stringify | ||
return JSON.stringify(pre.stage) === JSON.stringify(post.stage); | ||
} | ||
} | ||
|
||
export const MessageRules: IPropertyRule<IntakeMessage>[] = [ | ||
new IntakeMessageSameCheese, | ||
new IntakeMessageSameWeapon, | ||
new IntakeMessageSameBase, | ||
new IntakeMessageSameLocation, | ||
new IntakeMessageSameStage, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { HgResponse } from "../types/hg"; | ||
import { IRule } from "./interfaces"; | ||
|
||
export class HgResponseBothRequireSuccess implements IRule<HgResponse> { | ||
readonly description = "Both responses should have a 'success' of 1"; | ||
isValid(pre: HgResponse, post: HgResponse): boolean { | ||
return pre.success === 1 && post.success === 1; | ||
} | ||
} | ||
|
||
export class HgResponsePreNeedsPage implements IRule<HgResponse> { | ||
readonly description = "Pre-response should have a 'page' field"; | ||
isValid(pre: HgResponse, post: HgResponse): boolean { | ||
return pre.page !== undefined && pre.page !== null; | ||
} | ||
} | ||
|
||
export class HgResponseActiveTurn implements IRule<HgResponse> { | ||
readonly description = "Post-response should have true 'active_turn'"; | ||
isValid(pre: HgResponse, post: HgResponse): boolean { | ||
return post.active_turn === true; | ||
} | ||
} | ||
|
||
export const ResponseRules: IRule<HgResponse>[] = [ | ||
new HgResponseBothRequireSuccess, | ||
new HgResponsePreNeedsPage, | ||
new HgResponseActiveTurn, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { User } from "../types/hg"; | ||
import { IRule } from "./interfaces"; | ||
|
||
class UserRequiredDifferences implements IRule<User> { | ||
readonly description = "Pre and post user's 'num_active_turns' and 'next_activeturn_seconds' should differ"; | ||
readonly required_differences: (keyof User)[] = [ | ||
"num_active_turns", | ||
"next_activeturn_seconds" | ||
] | ||
|
||
isValid(pre: User, post: User): boolean { | ||
return this.required_differences.every(key => pre[key] != post[key]); | ||
} | ||
} | ||
|
||
class UserNumActiveTurnsIncrementedByOne implements IRule<User> { | ||
readonly description = "User number of active turns should increase by 1"; | ||
isValid(pre: User, post: User): boolean { | ||
return post.num_active_turns - pre.num_active_turns === 1; | ||
} | ||
} | ||
|
||
export const UserRules: IRule<User>[] = [ | ||
new UserRequiredDifferences, | ||
new UserNumActiveTurnsIncrementedByOne | ||
] |
Oops, something went wrong.