mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-01-14 16:08:02 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631e21452c | ||
|
|
be6fa4dd50 | ||
|
|
0d7a82836f | ||
|
|
d9a59b6824 | ||
|
|
ddbf8c3189 | ||
|
|
8393c471b2 | ||
|
|
fe66a2e8f7 | ||
|
|
4b0284102d | ||
|
|
95529f14a8 | ||
|
|
26af2c4e4d | ||
|
|
044c293f34 | ||
|
|
a082c9e593 | ||
|
|
4f3685a1f5 | ||
|
|
e242c36c09 |
10
Dockerfile
10
Dockerfile
@@ -4,6 +4,12 @@ ENV TZ=Etc/GMT
|
||||
|
||||
RUN apk update
|
||||
|
||||
# required dependencies in order to compile linux-musl (node-canvas) on alpine
|
||||
# https://github.com/node-gfx/node-canvas-prebuilt/issues/77#issuecomment-884365161
|
||||
RUN apk add --no-cache build-base g++ cairo-dev jpeg-dev pango-dev giflib-dev
|
||||
# required dependencies in order to compile linux-musl (node-canvas) on alpine
|
||||
RUN apk add --update --repository http://dl-3.alpinelinux.org/alpine/edge/testing libmount ttf-dejavu ttf-droid ttf-freefont ttf-liberation ttf-ubuntu-font-family fontconfig
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
WORKDIR /usr/app
|
||||
@@ -11,7 +17,9 @@ WORKDIR /usr/app
|
||||
COPY package*.json ./
|
||||
COPY tsconfig.json .
|
||||
|
||||
RUN npm install
|
||||
# no prebuild support for node-canvas on alpine so need to compile
|
||||
# https://github.com/Automattic/node-canvas#compiling
|
||||
RUN npm install --build-from-source
|
||||
|
||||
ADD . /usr/app
|
||||
|
||||
|
||||
@@ -62,6 +62,6 @@ commit_parsers = [
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
|
||||
1143
package-lock.json
generated
1143
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@
|
||||
"got": "^11.8.2",
|
||||
"he": "^1.2.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"image-size": "^1.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -65,6 +66,7 @@
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pretty-print-json": "^1.0.3",
|
||||
"resemblejs": "^4.0.0",
|
||||
"safe-stable-stringify": "^1.1.1",
|
||||
"snoostorm": "^1.5.2",
|
||||
"snoowrap": "^1.23.0",
|
||||
@@ -101,10 +103,15 @@
|
||||
"@types/object-hash": "^2.1.0",
|
||||
"@types/passport": "^1.0.7",
|
||||
"@types/passport-jwt": "^3.0.6",
|
||||
"@types/pixelmatch": "^5.2.4",
|
||||
"@types/resemblejs": "^3.2.1",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"ts-auto-guard": "*",
|
||||
"ts-json-schema-generator": "^0.93.0",
|
||||
"typescript-json-schema": "^0.50.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-canvas": "^2.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,31 @@
|
||||
import {HistoricalStats} from "./interfaces";
|
||||
|
||||
export const cacheOptDefaults = {ttl: 60, max: 500, checkPeriod: 600};
|
||||
export const cacheTTLDefaults = {authorTTL: 60, userNotesTTL: 300, wikiTTL: 300, submissionTTL: 60, commentTTL: 60, filterCriteriaTTL: 60, subredditTTL: 600};
|
||||
export const historicalDefaults: HistoricalStats = {
|
||||
eventsCheckedTotal: 0,
|
||||
eventsActionedTotal: 0,
|
||||
checksRun: new Map(),
|
||||
checksFromCache: new Map(),
|
||||
checksTriggered: new Map(),
|
||||
rulesRun: new Map(),
|
||||
//rulesCached: new Map(),
|
||||
rulesCachedTotal: 0,
|
||||
rulesTriggered: new Map(),
|
||||
actionsRun: new Map(),
|
||||
}
|
||||
|
||||
export const createHistoricalDefaults = (): HistoricalStats => {
|
||||
return {
|
||||
eventsCheckedTotal: 0,
|
||||
eventsActionedTotal: 0,
|
||||
checksRun: new Map(),
|
||||
checksFromCache: new Map(),
|
||||
checksTriggered: new Map(),
|
||||
rulesRun: new Map(),
|
||||
//rulesCached: new Map(),
|
||||
rulesCachedTotal: 0,
|
||||
rulesTriggered: new Map(),
|
||||
actionsRun: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Poll from "snoostorm/out/util/Poll";
|
||||
import Snoowrap from "snoowrap";
|
||||
import {RuleResult} from "../Rule";
|
||||
import {IncomingMessage} from "http";
|
||||
import {ResembleSingleCallbackComparisonResult} from "resemblejs";
|
||||
|
||||
/**
|
||||
* An ISO 8601 Duration
|
||||
@@ -224,6 +225,59 @@ export interface ReferenceSubmission {
|
||||
useSubmissionAsReference?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.
|
||||
*
|
||||
* **Note:** This is an **experimental feature**
|
||||
* */
|
||||
export interface ImageDetection {
|
||||
/**
|
||||
* Is image detection enabled?
|
||||
* */
|
||||
enable?: boolean
|
||||
/**
|
||||
* Determines how and when to check if a URL is an image
|
||||
*
|
||||
* **Note:** After fetching a URL the **Content-Type** is validated to contain `image` before detection occurs
|
||||
*
|
||||
* **When `extension`:** (default)
|
||||
*
|
||||
* * Only URLs that end in known image extensions (.png, .jpg, etc...) are fetched
|
||||
*
|
||||
* **When `unknown`:**
|
||||
*
|
||||
* * URLs that end in known image extensions (.png, .jpg, etc...) are fetched
|
||||
* * URLs with no extension or unknown (IE non-video, non-doc, etc...) are fetched
|
||||
*
|
||||
* **When `all`:**
|
||||
*
|
||||
* * All submissions that have URLs (non-self) will be fetched, regardless of extension
|
||||
* * **Note:** This can be bandwidth/CPU intensive if history window is large so use with care
|
||||
*
|
||||
* @default "extension"
|
||||
* */
|
||||
fetchBehavior?: 'extension' | 'unknown' | 'all',
|
||||
/**
|
||||
* The percentage, as a whole number, of pixels that are **different** between the two images at which point the images are not considered the same.
|
||||
*
|
||||
* Default is `5`
|
||||
*
|
||||
* @default 5
|
||||
* */
|
||||
threshold?: number
|
||||
}
|
||||
|
||||
export interface ImageData {
|
||||
data: Buffer,
|
||||
width: number,
|
||||
height: number
|
||||
pixels: number
|
||||
}
|
||||
|
||||
export interface ResembleResult extends ResembleSingleCallbackComparisonResult {
|
||||
rawMisMatchPercentage: number
|
||||
}
|
||||
|
||||
export interface RichContent {
|
||||
/**
|
||||
* The Content to submit for this Action. Content is interpreted as reddit-flavored Markdown.
|
||||
@@ -1556,3 +1610,83 @@ export interface StatusCodeError extends Error {
|
||||
response: IncomingMessage,
|
||||
error: Error
|
||||
}
|
||||
|
||||
export interface HistoricalStatsDisplay extends HistoricalStats {
|
||||
checksRunTotal: number
|
||||
checksFromCacheTotal: number
|
||||
checksTriggeredTotal: number
|
||||
rulesRunTotal: number
|
||||
rulesCachedTotal: number
|
||||
rulesTriggeredTotal: number
|
||||
actionsRunTotal: number
|
||||
}
|
||||
|
||||
export interface HistoricalStats {
|
||||
eventsCheckedTotal: number
|
||||
eventsActionedTotal: number
|
||||
checksRun: Map<string, number>
|
||||
checksFromCache: Map<string, number>
|
||||
checksTriggered: Map<string, number>
|
||||
rulesRun: Map<string, number>
|
||||
//rulesCached: Map<string, number>
|
||||
rulesCachedTotal: number
|
||||
rulesTriggered: Map<string, number>
|
||||
actionsRun: Map<string, number>
|
||||
[index: string]: any
|
||||
}
|
||||
|
||||
export interface SubredditHistoricalStats {
|
||||
allTime: HistoricalStats
|
||||
lastReload: HistoricalStats
|
||||
}
|
||||
|
||||
export interface SubredditHistoricalStatsDisplay {
|
||||
allTime: HistoricalStatsDisplay
|
||||
lastReload: HistoricalStatsDisplay
|
||||
}
|
||||
|
||||
export interface ManagerStats {
|
||||
// eventsCheckedTotal: number
|
||||
// eventsCheckedSinceStartTotal: number
|
||||
eventsAvg: number
|
||||
// checksRunTotal: number
|
||||
// checksRunSinceStartTotal: number
|
||||
// checksTriggered: number
|
||||
// checksTriggeredTotal: number
|
||||
// checksTriggeredSinceStart: number
|
||||
// checksTriggeredSinceStartTotal: number
|
||||
// rulesRunTotal: number
|
||||
// rulesRunSinceStartTotal: number
|
||||
// rulesCachedTotal: number
|
||||
// rulesCachedSinceStartTotal: number
|
||||
// rulesTriggeredTotal: number
|
||||
// rulesTriggeredSinceStartTotal: number
|
||||
rulesAvg: number
|
||||
// actionsRun: number
|
||||
// actionsRunTotal: number
|
||||
// actionsRunSinceStart: number,
|
||||
// actionsRunSinceStartTotal: number
|
||||
historical: SubredditHistoricalStatsDisplay
|
||||
cache: {
|
||||
provider: string,
|
||||
currentKeyCount: number,
|
||||
isShared: boolean,
|
||||
totalRequests: number,
|
||||
totalMiss: number,
|
||||
missPercent: string,
|
||||
requestRate: number,
|
||||
types: ResourceStats
|
||||
},
|
||||
}
|
||||
|
||||
export interface HistoricalStatUpdateData {
|
||||
eventsCheckedTotal?: number
|
||||
eventsActionedTotal?: number
|
||||
checksRun: string[] | string
|
||||
checksTriggered: string[] | string
|
||||
checksFromCache: string[] | string
|
||||
actionsRun: string[] | string
|
||||
rulesRun: string[] | string
|
||||
rulesCachedTotal: number
|
||||
rulesTriggered: string[] | string
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface AttributionCriteria {
|
||||
* * If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`
|
||||
* * If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit
|
||||
*
|
||||
* If nothing is specified or list is empty (default) all domains are aggregated
|
||||
* If nothing is specified or list is empty (default) rule will only aggregate on `link` and `media` (ignores reddit-hosted content and self-posts)
|
||||
*
|
||||
* @default undefined
|
||||
* @examples [[]]
|
||||
@@ -174,7 +174,7 @@ export class AttributionRule extends Rule {
|
||||
window,
|
||||
thresholdOn = 'all',
|
||||
minActivityCount = 10,
|
||||
aggregateOn = [],
|
||||
aggregateOn = ['link','media'],
|
||||
consolidateMediaDomains = false,
|
||||
domains = [],
|
||||
domainsCombined = false,
|
||||
@@ -392,7 +392,7 @@ export class AttributionRule extends Rule {
|
||||
|
||||
}
|
||||
|
||||
interface AttributionConfig extends ReferenceSubmission {
|
||||
interface AttributionConfig {
|
||||
|
||||
/**
|
||||
* A list threshold-window values to test attribution against
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import {Rule, RuleJSONConfig, RuleOptions, RulePremise, RuleResult} from "./index";
|
||||
import {Comment, VoteableContent} from "snoowrap";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
// @ts-ignore
|
||||
import subImageMatch from 'matches-subimage';
|
||||
import {
|
||||
activityWindowText, asSubmission,
|
||||
comparisonTextOp, FAIL, formatNumber, getActivitySubredditName, isSubmission, objectToStringSummary,
|
||||
parseGenericValueOrPercentComparison, parseStringToRegex, parseSubredditName,
|
||||
activityWindowText,
|
||||
asSubmission, compareImages,
|
||||
comparisonTextOp,
|
||||
FAIL,
|
||||
formatNumber,
|
||||
getActivitySubredditName, getImageDataFromUrl,
|
||||
isSubmission,
|
||||
isValidImageURL,
|
||||
objectToStringSummary,
|
||||
parseGenericValueOrPercentComparison,
|
||||
parseStringToRegex,
|
||||
parseSubredditName,
|
||||
parseUsableLinkIdentifier,
|
||||
PASS, toStrongSubredditState
|
||||
PASS,
|
||||
toStrongSubredditState
|
||||
} from "../util";
|
||||
import {
|
||||
ActivityWindow,
|
||||
ActivityWindowCriteria,
|
||||
ActivityWindowType, CommentState,
|
||||
ActivityWindowType, CommentState, ImageDetection,
|
||||
ReferenceSubmission, StrongSubredditState, SubmissionState,
|
||||
SubredditCriteria, SubredditState
|
||||
} from "../Common/interfaces";
|
||||
import {SubredditResources} from "../Subreddit/SubredditResources";
|
||||
|
||||
const parseLink = parseUsableLinkIdentifier();
|
||||
|
||||
@@ -23,6 +34,7 @@ export class RecentActivityRule extends Rule {
|
||||
window: ActivityWindowType;
|
||||
thresholds: ActivityThreshold[];
|
||||
useSubmissionAsReference: boolean;
|
||||
imageDetection: Required<ImageDetection>
|
||||
lookAt?: 'comments' | 'submissions';
|
||||
|
||||
constructor(options: RecentActivityRuleOptions) {
|
||||
@@ -30,8 +42,21 @@ export class RecentActivityRule extends Rule {
|
||||
const {
|
||||
window = 15,
|
||||
useSubmissionAsReference = true,
|
||||
imageDetection,
|
||||
lookAt,
|
||||
} = options || {};
|
||||
|
||||
const {
|
||||
enable = false,
|
||||
fetchBehavior = 'extension',
|
||||
threshold = 5
|
||||
} = imageDetection || {};
|
||||
|
||||
this.imageDetection = {
|
||||
enable,
|
||||
fetchBehavior,
|
||||
threshold
|
||||
};
|
||||
this.lookAt = lookAt;
|
||||
this.useSubmissionAsReference = useSubmissionAsReference;
|
||||
this.window = window;
|
||||
@@ -73,16 +98,56 @@ export class RecentActivityRule extends Rule {
|
||||
} else if (item.is_self) {
|
||||
this.logger.warn('Cannot use post as reference because triggered Submission is not a link type');
|
||||
} else {
|
||||
const usableUrl = parseLink(await item.url);
|
||||
viableActivity = viableActivity.filter((x) => {
|
||||
if (!asSubmission(x)) {
|
||||
return false;
|
||||
const itemId = item.id;
|
||||
const referenceUrl = await item.url;
|
||||
const usableUrl = parseLink(referenceUrl);
|
||||
const filteredActivity = [];
|
||||
let referenceImage;
|
||||
if(this.imageDetection.enable) {
|
||||
const [response, imgData, reason] = await getImageDataFromUrl(referenceUrl);
|
||||
if(reason !== undefined) {
|
||||
this.logger.verbose(reason);
|
||||
} else {
|
||||
referenceImage = imgData
|
||||
}
|
||||
}
|
||||
let longRun;
|
||||
if(referenceImage !== undefined) {
|
||||
const l = this.logger;
|
||||
longRun = setTimeout(() => {
|
||||
l.verbose('FYI: Image processing is causing rule to take longer than normal');
|
||||
}, 2500);
|
||||
}
|
||||
for(const x of viableActivity) {
|
||||
if (!asSubmission(x) || x.id === itemId) {
|
||||
continue;
|
||||
}
|
||||
if (x.url === undefined) {
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
return parseLink(x.url) === usableUrl;
|
||||
});
|
||||
if(parseLink(x.url) === usableUrl) {
|
||||
filteredActivity.push(x);
|
||||
}
|
||||
// only do image detection if regular URL comparison and other conditions fail first
|
||||
// to reduce CPU/bandwidth usage
|
||||
if(referenceImage !== undefined) {
|
||||
const [response, imgData, reason] = await getImageDataFromUrl(x.url);
|
||||
if(imgData !== undefined) {
|
||||
try {
|
||||
const [compareResult, sameImage] = await compareImages(referenceImage, imgData, this.imageDetection.threshold);
|
||||
if(sameImage) {
|
||||
filteredActivity.push(x);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Unexpected error encountered while comparing images, will skip comparison: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(longRun !== undefined) {
|
||||
clearTimeout(longRun);
|
||||
}
|
||||
viableActivity = filteredActivity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +353,8 @@ interface RecentActivityConfig extends ActivityWindow, ReferenceSubmission {
|
||||
* @minItems 1
|
||||
* */
|
||||
thresholds: ActivityThreshold[],
|
||||
|
||||
imageDetection?: ImageDetection
|
||||
}
|
||||
|
||||
export interface RecentActivityRuleOptions extends RecentActivityConfig, RuleOptions {
|
||||
|
||||
@@ -28,6 +28,7 @@ interface ResultContext {
|
||||
|
||||
export interface RuleResult extends ResultContext {
|
||||
premise: RulePremise
|
||||
kind: string
|
||||
name: string
|
||||
triggered: (boolean | null)
|
||||
}
|
||||
@@ -153,6 +154,7 @@ export abstract class Rule implements IRule, Triggerable {
|
||||
protected getResult(triggered: (boolean | null) = null, context: ResultContext = {}): RuleResult {
|
||||
return {
|
||||
premise: this.getPremise(),
|
||||
kind: this.getKind(),
|
||||
name: this.name,
|
||||
triggered,
|
||||
...context,
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) rule will only aggregate on `link` and `media` (ignores reddit-hosted content and self-posts)",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -419,11 +419,6 @@
|
||||
],
|
||||
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"useSubmissionAsReference": {
|
||||
"default": true,
|
||||
"description": "If activity is a Submission and is a link (not self-post) then only look at Submissions that contain this link, otherwise consider all activities.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1609,6 +1604,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetection": {
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Is image detection enabled?",
|
||||
"type": "boolean"
|
||||
},
|
||||
"fetchBehavior": {
|
||||
"default": "extension",
|
||||
"description": "Determines how and when to check if a URL is an image\n\n**Note:** After fetching a URL the **Content-Type** is validated to contain `image` before detection occurs\n\n**When `extension`:** (default)\n\n* Only URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n\n**When `unknown`:**\n\n* URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n* URLs with no extension or unknown (IE non-video, non-doc, etc...) are fetched\n\n**When `all`:**\n\n* All submissions that have URLs (non-self) will be fetched, regardless of extension\n* **Note:** This can be bandwidth/CPU intensive if history window is large so use with care",
|
||||
"enum": [
|
||||
"all",
|
||||
"extension",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"threshold": {
|
||||
"default": 5,
|
||||
"description": "The percentage, as a whole number, of pixels that are **different** between the two images at which point the images are not considered the same.\n\nDefault is `5`",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"LockActionJson": {
|
||||
"description": "Lock the Activity",
|
||||
"properties": {
|
||||
@@ -1934,6 +1954,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"imageDetection": {
|
||||
"$ref": "#/definitions/ImageDetection",
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) rule will only aggregate on `link` and `media` (ignores reddit-hosted content and self-posts)",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -365,11 +365,6 @@
|
||||
],
|
||||
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"useSubmissionAsReference": {
|
||||
"default": true,
|
||||
"description": "If activity is a Submission and is a link (not self-post) then only look at Submissions that contain this link, otherwise consider all activities.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -868,6 +863,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetection": {
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Is image detection enabled?",
|
||||
"type": "boolean"
|
||||
},
|
||||
"fetchBehavior": {
|
||||
"default": "extension",
|
||||
"description": "Determines how and when to check if a URL is an image\n\n**Note:** After fetching a URL the **Content-Type** is validated to contain `image` before detection occurs\n\n**When `extension`:** (default)\n\n* Only URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n\n**When `unknown`:**\n\n* URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n* URLs with no extension or unknown (IE non-video, non-doc, etc...) are fetched\n\n**When `all`:**\n\n* All submissions that have URLs (non-self) will be fetched, regardless of extension\n* **Note:** This can be bandwidth/CPU intensive if history window is large so use with care",
|
||||
"enum": [
|
||||
"all",
|
||||
"extension",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"threshold": {
|
||||
"default": 5,
|
||||
"description": "The percentage, as a whole number, of pixels that are **different** between the two images at which point the images are not considered the same.\n\nDefault is `5`",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RecentActivityRuleJSONConfig": {
|
||||
"description": "Checks a user's history for any Activity (Submission/Comment) in the subreddits specified in thresholds\n\nAvailable data for [Action templating](https://github.com/FoxxMD/context-mod#action-templating):\n\n```\nsummary => comma-deliminated list of subreddits that hit the threshold and their count EX subredditA(1), subredditB(4),...\nsubCount => Total number of subreddits that hit the threshold\ntotalCount => Total number of all activity occurrences in subreddits\n```",
|
||||
"properties": {
|
||||
@@ -890,6 +910,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"imageDetection": {
|
||||
"$ref": "#/definitions/ImageDetection",
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) rule will only aggregate on `link` and `media` (ignores reddit-hosted content and self-posts)",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -342,11 +342,6 @@
|
||||
],
|
||||
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
|
||||
"type": "string"
|
||||
},
|
||||
"useSubmissionAsReference": {
|
||||
"default": true,
|
||||
"description": "If activity is a Submission and is a link (not self-post) then only look at Submissions that contain this link, otherwise consider all activities.",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -845,6 +840,31 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetection": {
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**",
|
||||
"properties": {
|
||||
"enable": {
|
||||
"description": "Is image detection enabled?",
|
||||
"type": "boolean"
|
||||
},
|
||||
"fetchBehavior": {
|
||||
"default": "extension",
|
||||
"description": "Determines how and when to check if a URL is an image\n\n**Note:** After fetching a URL the **Content-Type** is validated to contain `image` before detection occurs\n\n**When `extension`:** (default)\n\n* Only URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n\n**When `unknown`:**\n\n* URLs that end in known image extensions (.png, .jpg, etc...) are fetched\n* URLs with no extension or unknown (IE non-video, non-doc, etc...) are fetched\n\n**When `all`:**\n\n* All submissions that have URLs (non-self) will be fetched, regardless of extension\n* **Note:** This can be bandwidth/CPU intensive if history window is large so use with care",
|
||||
"enum": [
|
||||
"all",
|
||||
"extension",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"threshold": {
|
||||
"default": 5,
|
||||
"description": "The percentage, as a whole number, of pixels that are **different** between the two images at which point the images are not considered the same.\n\nDefault is `5`",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"RecentActivityRuleJSONConfig": {
|
||||
"description": "Checks a user's history for any Activity (Submission/Comment) in the subreddits specified in thresholds\n\nAvailable data for [Action templating](https://github.com/FoxxMD/context-mod#action-templating):\n\n```\nsummary => comma-deliminated list of subreddits that hit the threshold and their count EX subredditA(1), subredditB(4),...\nsubCount => Total number of subreddits that hit the threshold\ntotalCount => Total number of all activity occurrences in subreddits\n```",
|
||||
"properties": {
|
||||
@@ -867,6 +887,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"imageDetection": {
|
||||
"$ref": "#/definitions/ImageDetection",
|
||||
"description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**"
|
||||
},
|
||||
"itemIs": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Logger} from "winston";
|
||||
import {SubmissionCheck} from "../Check/SubmissionCheck";
|
||||
import {CommentCheck} from "../Check/CommentCheck";
|
||||
import {
|
||||
cacheStats,
|
||||
cacheStats, createHistoricalStatsDisplay,
|
||||
createRetryHandler,
|
||||
determineNewResults, findLastIndex, formatNumber,
|
||||
mergeArr, parseFromJsonOrYamlToObject, pollingInfo, resultsSummary, sleep, totalFromMapStats, triggeredIndicator,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ActionResult,
|
||||
DEFAULT_POLLING_INTERVAL,
|
||||
DEFAULT_POLLING_LIMIT, Invokee,
|
||||
ManagerOptions, ManagerStateChangeOption, PAUSED,
|
||||
ManagerOptions, ManagerStateChangeOption, ManagerStats, PAUSED,
|
||||
PollingOptionsStrong, ResourceStats, RUNNING, RunState, STOPPED, SYSTEM, USER
|
||||
} from "../Common/interfaces";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
@@ -39,6 +39,7 @@ import {JSONConfig} from "../JsonConfig";
|
||||
import {CheckStructuredJson} from "../Check";
|
||||
import NotificationManager from "../Notification/NotificationManager";
|
||||
import action from "../Web/Server/routes/authenticated/user/action";
|
||||
import {createHistoricalDefaults, historicalDefaults} from "../Common/defaults";
|
||||
|
||||
export interface RunningState {
|
||||
state: RunState,
|
||||
@@ -65,39 +66,6 @@ export interface RuntimeManagerOptions extends ManagerOptions {
|
||||
maxWorkers: number;
|
||||
}
|
||||
|
||||
export interface ManagerStats {
|
||||
eventsCheckedTotal: number
|
||||
eventsCheckedSinceStartTotal: number
|
||||
eventsAvg: number
|
||||
checksRunTotal: number
|
||||
checksRunSinceStartTotal: number
|
||||
checksTriggered: number
|
||||
checksTriggeredTotal: number
|
||||
checksTriggeredSinceStart: number
|
||||
checksTriggeredSinceStartTotal: number
|
||||
rulesRunTotal: number
|
||||
rulesRunSinceStartTotal: number
|
||||
rulesCachedTotal: number
|
||||
rulesCachedSinceStartTotal: number
|
||||
rulesTriggeredTotal: number
|
||||
rulesTriggeredSinceStartTotal: number
|
||||
rulesAvg: number
|
||||
actionsRun: number
|
||||
actionsRunTotal: number
|
||||
actionsRunSinceStart: number,
|
||||
actionsRunSinceStartTotal: number
|
||||
cache: {
|
||||
provider: string,
|
||||
currentKeyCount: number,
|
||||
isShared: boolean,
|
||||
totalRequests: number,
|
||||
totalMiss: number,
|
||||
missPercent: string,
|
||||
requestRate: number,
|
||||
types: ResourceStats
|
||||
},
|
||||
}
|
||||
|
||||
interface QueuedIdentifier {
|
||||
id: string,
|
||||
shouldRefresh: boolean
|
||||
@@ -164,50 +132,22 @@ export class Manager {
|
||||
// use by api nanny to slow event consumption
|
||||
delayBy?: number;
|
||||
|
||||
eventsCheckedTotal: number = 0;
|
||||
eventsCheckedSinceStartTotal: number = 0;
|
||||
eventsSample: number[] = [];
|
||||
eventsSampleInterval: any;
|
||||
eventsRollingAvg: number = 0;
|
||||
checksRunTotal: number = 0;
|
||||
checksRunSinceStartTotal: number = 0;
|
||||
checksTriggered: Map<string, number> = new Map();
|
||||
checksTriggeredSinceStart: Map<string, number> = new Map();
|
||||
rulesRunTotal: number = 0;
|
||||
rulesRunSinceStartTotal: number = 0;
|
||||
rulesCachedTotal: number = 0;
|
||||
rulesCachedSinceStartTotal: number = 0;
|
||||
rulesTriggeredTotal: number = 0;
|
||||
rulesTriggeredSinceStartTotal: number = 0;
|
||||
rulesUniqueSample: number[] = [];
|
||||
rulesUniqueSampleInterval: any;
|
||||
rulesUniqueRollingAvg: number = 0;
|
||||
actionsRun: Map<string, number> = new Map();
|
||||
actionsRunSinceStart: Map<string, number> = new Map();
|
||||
actionedEvents: ActionedEvent[] = [];
|
||||
|
||||
getStats = async (): Promise<ManagerStats> => {
|
||||
const data: any = {
|
||||
eventsCheckedTotal: this.eventsCheckedTotal,
|
||||
eventsCheckedSinceStartTotal: this.eventsCheckedSinceStartTotal,
|
||||
eventsAvg: formatNumber(this.eventsRollingAvg),
|
||||
checksRunTotal: this.checksRunTotal,
|
||||
checksRunSinceStartTotal: this.checksRunSinceStartTotal,
|
||||
checksTriggered: this.checksTriggered,
|
||||
checksTriggeredTotal: totalFromMapStats(this.checksTriggered),
|
||||
checksTriggeredSinceStart: this.checksTriggeredSinceStart,
|
||||
checksTriggeredSinceStartTotal: totalFromMapStats(this.checksTriggeredSinceStart),
|
||||
rulesRunTotal: this.rulesRunTotal,
|
||||
rulesRunSinceStartTotal: this.rulesRunSinceStartTotal,
|
||||
rulesCachedTotal: this.rulesCachedTotal,
|
||||
rulesCachedSinceStartTotal: this.rulesCachedSinceStartTotal,
|
||||
rulesTriggeredTotal: this.rulesTriggeredTotal,
|
||||
rulesTriggeredSinceStartTotal: this.rulesTriggeredSinceStartTotal,
|
||||
rulesAvg: formatNumber(this.rulesUniqueRollingAvg),
|
||||
actionsRun: this.actionsRun,
|
||||
actionsRunTotal: totalFromMapStats(this.actionsRun),
|
||||
actionsRunSinceStart: this.actionsRunSinceStart,
|
||||
actionsRunSinceStartTotal: totalFromMapStats(this.actionsRunSinceStart),
|
||||
historical: {
|
||||
lastReload: createHistoricalStatsDisplay(createHistoricalDefaults()),
|
||||
allTime: createHistoricalStatsDisplay(createHistoricalDefaults()),
|
||||
},
|
||||
cache: {
|
||||
provider: 'none',
|
||||
currentKeyCount: 0,
|
||||
@@ -223,6 +163,7 @@ export class Manager {
|
||||
if (this.resources !== undefined) {
|
||||
const resStats = await this.resources.getStats();
|
||||
|
||||
data.historical = this.resources.getHistoricalDisplayStats();
|
||||
data.cache = resStats.cache;
|
||||
data.cache.currentKeyCount = await this.resources.getCacheKeyCount();
|
||||
data.cache.isShared = this.resources.cacheSettingsHash === 'default';
|
||||
@@ -270,8 +211,9 @@ export class Manager {
|
||||
|
||||
this.eventsSampleInterval = setInterval((function(self) {
|
||||
return function() {
|
||||
const et = self.resources !== undefined ? self.resources.stats.historical.allTime.eventsCheckedTotal : 0;
|
||||
const rollingSample = self.eventsSample.slice(0, 7)
|
||||
rollingSample.unshift(self.eventsCheckedTotal)
|
||||
rollingSample.unshift(et)
|
||||
self.eventsSample = rollingSample;
|
||||
const diff = self.eventsSample.reduceRight((acc: number[], curr, index) => {
|
||||
if(self.eventsSample[index + 1] !== undefined) {
|
||||
@@ -291,7 +233,8 @@ export class Manager {
|
||||
this.rulesUniqueSampleInterval = setInterval((function(self) {
|
||||
return function() {
|
||||
const rollingSample = self.rulesUniqueSample.slice(0, 7)
|
||||
rollingSample.unshift(self.rulesRunTotal - self.rulesCachedTotal);
|
||||
const rt = self.resources !== undefined ? self.resources.stats.historical.allTime.rulesRunTotal - self.resources.stats.historical.allTime.rulesCachedTotal : 0;
|
||||
rollingSample.unshift(rt);
|
||||
self.rulesUniqueSample = rollingSample;
|
||||
const diff = self.rulesUniqueSample.reduceRight((acc: number[], curr, index) => {
|
||||
if(self.rulesUniqueSample[index + 1] !== undefined) {
|
||||
@@ -387,7 +330,7 @@ export class Manager {
|
||||
return q;
|
||||
}
|
||||
|
||||
protected parseConfigurationFromObject(configObj: object) {
|
||||
protected async parseConfigurationFromObject(configObj: object) {
|
||||
try {
|
||||
const configBuilder = new ConfigBuilder({logger: this.logger});
|
||||
const validJson = configBuilder.validateJson(configObj);
|
||||
@@ -437,7 +380,7 @@ export class Manager {
|
||||
caching,
|
||||
client: this.client,
|
||||
};
|
||||
this.resources = this.cacheManager.set(this.subreddit.display_name, resourceConfig);
|
||||
this.resources = await this.cacheManager.set(this.subreddit.display_name, resourceConfig);
|
||||
this.resources.setLogger(this.logger);
|
||||
|
||||
this.logger.info('Subreddit-specific options updated');
|
||||
@@ -532,7 +475,7 @@ export class Manager {
|
||||
throw new ConfigParseError('Could not parse wiki page contents as JSON or YAML')
|
||||
}
|
||||
|
||||
this.parseConfigurationFromObject(configObj);
|
||||
await this.parseConfigurationFromObject(configObj);
|
||||
this.logger.info('Checks updated');
|
||||
|
||||
if(!suppressNotification) {
|
||||
@@ -549,8 +492,6 @@ export class Manager {
|
||||
async runChecks(checkType: ('Comment' | 'Submission'), activity: (Submission | Comment), options?: runCheckOptions): Promise<void> {
|
||||
const checks = checkType === 'Comment' ? this.commentChecks : this.submissionChecks;
|
||||
let item = activity;
|
||||
this.eventsCheckedTotal++;
|
||||
this.eventsCheckedSinceStartTotal++;
|
||||
const itemId = await item.id;
|
||||
let allRuleResults: RuleResult[] = [];
|
||||
const itemIdentifier = `${checkType === 'Submission' ? 'SUB' : 'COM'} ${itemId}`;
|
||||
@@ -622,7 +563,9 @@ export class Manager {
|
||||
actionResults: [],
|
||||
}
|
||||
let triggered = false;
|
||||
|
||||
let triggeredCheckName;
|
||||
const checksRunNames = [];
|
||||
const cachedCheckNames = [];
|
||||
try {
|
||||
for (const check of checks) {
|
||||
if (checkNames.length > 0 && !checkNames.map(x => x.toLowerCase()).some(x => x === check.name.toLowerCase())) {
|
||||
@@ -633,6 +576,7 @@ export class Manager {
|
||||
this.logger.info(`Check ${check.name} not run because it is not enabled, skipping...`);
|
||||
continue;
|
||||
}
|
||||
checksRunNames.push(check.name);
|
||||
checksRun++;
|
||||
triggered = false;
|
||||
let isFromCache = false;
|
||||
@@ -642,6 +586,8 @@ export class Manager {
|
||||
isFromCache = fromCache;
|
||||
if(!fromCache) {
|
||||
await check.setCacheResult(item, {result: checkTriggered, ruleResults: checkResults});
|
||||
} else {
|
||||
cachedCheckNames.push(check.name);
|
||||
}
|
||||
currentResults = checkResults;
|
||||
totalRulesRun += checkResults.length;
|
||||
@@ -658,6 +604,7 @@ export class Manager {
|
||||
}
|
||||
|
||||
if (triggered) {
|
||||
triggeredCheckName = check.name;
|
||||
actionedEvent.check = check.name;
|
||||
actionedEvent.ruleResults = currentResults;
|
||||
if(isFromCache) {
|
||||
@@ -665,8 +612,6 @@ export class Manager {
|
||||
} else {
|
||||
actionedEvent.ruleSummary = resultsSummary(currentResults, check.condition);
|
||||
}
|
||||
this.checksTriggered.set(check.name, (this.checksTriggered.get(check.name) || 0) + 1);
|
||||
this.checksTriggeredSinceStart.set(check.name, (this.checksTriggeredSinceStart.get(check.name) || 0) + 1);
|
||||
runActions = await check.runActions(item, currentResults.filter(x => x.triggered), dryRun);
|
||||
actionsRun = runActions.length;
|
||||
|
||||
@@ -688,23 +633,6 @@ export class Manager {
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
const cachedTotal = totalRulesRun - allRuleResults.length;
|
||||
const triggeredRulesTotal = allRuleResults.filter(x => x.triggered).length;
|
||||
|
||||
this.checksRunTotal += checksRun;
|
||||
this.checksRunSinceStartTotal += checksRun;
|
||||
this.rulesRunTotal += totalRulesRun;
|
||||
this.rulesRunSinceStartTotal += totalRulesRun;
|
||||
this.rulesCachedTotal += cachedTotal;
|
||||
this.rulesCachedSinceStartTotal += cachedTotal;
|
||||
this.rulesTriggeredTotal += triggeredRulesTotal;
|
||||
this.rulesTriggeredSinceStartTotal += triggeredRulesTotal;
|
||||
|
||||
for (const a of runActions) {
|
||||
const name = a.name;
|
||||
this.actionsRun.set(name, (this.actionsRun.get(name) || 0) + 1);
|
||||
this.actionsRunSinceStart.set(name, (this.actionsRunSinceStart.get(name) || 0) + 1);
|
||||
}
|
||||
actionedEvent.actionResults = runActions;
|
||||
if(triggered) {
|
||||
await this.resources.addActionedEvent(actionedEvent);
|
||||
@@ -715,6 +643,18 @@ export class Manager {
|
||||
this.currentLabels = [];
|
||||
} catch (err) {
|
||||
this.logger.error('Error occurred while cleaning up Activity check and generating stats', err);
|
||||
} finally {
|
||||
this.resources.updateHistoricalStats({
|
||||
eventsCheckedTotal: 1,
|
||||
eventsActionedTotal: triggered ? 1 : 0,
|
||||
checksTriggered: triggeredCheckName !== undefined ? [triggeredCheckName] : [],
|
||||
checksRun: checksRunNames,
|
||||
checksFromCache: cachedCheckNames,
|
||||
actionsRun: runActions.map(x => x.name),
|
||||
rulesRun: allRuleResults.map(x => x.name),
|
||||
rulesTriggered: allRuleResults.filter(x => x.triggered).map(x => x.name),
|
||||
rulesCachedTotal: totalRulesRun - allRuleResults.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1002,13 +942,6 @@ export class Manager {
|
||||
stream.removeListener('item', v);
|
||||
}
|
||||
this.startedAt = undefined;
|
||||
this.eventsCheckedSinceStartTotal = 0;
|
||||
this.checksRunSinceStartTotal = 0;
|
||||
this.rulesRunSinceStartTotal = 0;
|
||||
this.rulesCachedSinceStartTotal = 0;
|
||||
this.rulesTriggeredSinceStartTotal = 0;
|
||||
this.checksTriggeredSinceStart = new Map();
|
||||
this.actionsRunSinceStart = new Map();
|
||||
this.logger.info(`Events STOPPED by ${causedBy}`);
|
||||
this.eventsState = {
|
||||
state: STOPPED,
|
||||
|
||||
@@ -13,7 +13,7 @@ import fetch from 'node-fetch';
|
||||
import {
|
||||
asSubmission,
|
||||
buildCacheOptionsFromProvider, buildCachePrefix,
|
||||
cacheStats, comparisonTextOp, createCacheManager,
|
||||
cacheStats, comparisonTextOp, createCacheManager, createHistoricalStatsDisplay,
|
||||
formatNumber, getActivityAuthorName, getActivitySubredditName, isStrongSubredditState,
|
||||
mergeArr,
|
||||
parseExternalUrl, parseGenericValueComparison,
|
||||
@@ -22,9 +22,21 @@ import {
|
||||
import LoggedError from "../Utils/LoggedError";
|
||||
import {
|
||||
BotInstanceConfig,
|
||||
CacheOptions, CommentState,
|
||||
Footer, OperatorConfig, ResourceStats, StrongCache, SubmissionState,
|
||||
CacheConfig, TTLConfig, TypedActivityStates, UserResultCache, ActionedEvent, SubredditState, StrongSubredditState
|
||||
CacheOptions,
|
||||
CommentState,
|
||||
Footer,
|
||||
OperatorConfig,
|
||||
ResourceStats,
|
||||
StrongCache,
|
||||
SubmissionState,
|
||||
CacheConfig,
|
||||
TTLConfig,
|
||||
TypedActivityStates,
|
||||
UserResultCache,
|
||||
ActionedEvent,
|
||||
SubredditState,
|
||||
StrongSubredditState,
|
||||
HistoricalStats, HistoricalStatUpdateData, SubredditHistoricalStats, SubredditHistoricalStatsDisplay
|
||||
} from "../Common/interfaces";
|
||||
import UserNotes from "./UserNotes";
|
||||
import Mustache from "mustache";
|
||||
@@ -33,7 +45,7 @@ import {AuthorCriteria} from "../Author/Author";
|
||||
import {SPoll} from "./Streams";
|
||||
import {Cache} from 'cache-manager';
|
||||
import {Submission, Comment} from "snoowrap/dist/objects";
|
||||
import {cacheTTLDefaults} from "../Common/defaults";
|
||||
import {cacheTTLDefaults, createHistoricalDefaults, historicalDefaults} from "../Common/defaults";
|
||||
import {check} from "tcp-port-used";
|
||||
|
||||
export const DEFAULT_FOOTER = '\r\n*****\r\nThis action was performed by [a bot.]({{botLink}}) Mention a moderator or [send a modmail]({{modmailLink}}) if you any ideas, questions, or concerns about this action.';
|
||||
@@ -79,10 +91,14 @@ export class SubredditResources {
|
||||
cacheType: string
|
||||
cacheSettingsHash?: string;
|
||||
pruneInterval?: any;
|
||||
historicalSaveInterval?: any;
|
||||
prefix?: string
|
||||
actionedEventsMax: number;
|
||||
|
||||
stats: { cache: ResourceStats };
|
||||
stats: {
|
||||
cache: ResourceStats
|
||||
historical: SubredditHistoricalStats
|
||||
};
|
||||
|
||||
constructor(name: string, options: SubredditResourceOptions) {
|
||||
const {
|
||||
@@ -127,7 +143,11 @@ export class SubredditResources {
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
cache: cacheStats()
|
||||
cache: cacheStats(),
|
||||
historical: {
|
||||
allTime: createHistoricalDefaults(),
|
||||
lastReload: createHistoricalDefaults()
|
||||
}
|
||||
};
|
||||
|
||||
const cacheUseCB = (miss: boolean) => {
|
||||
@@ -151,6 +171,92 @@ export class SubredditResources {
|
||||
}
|
||||
}
|
||||
|
||||
async initHistoricalStats() {
|
||||
const at = await this.cache.wrap(`${this.name}-historical-allTime`, () => createHistoricalDefaults(), {ttl: 0}) as object;
|
||||
const rehydratedAt: any = {};
|
||||
for(const [k, v] of Object.entries(at)) {
|
||||
if(Array.isArray(v)) {
|
||||
rehydratedAt[k] = new Map(v);
|
||||
} else {
|
||||
rehydratedAt[k] = v;
|
||||
}
|
||||
}
|
||||
this.stats.historical.allTime = rehydratedAt as HistoricalStats;
|
||||
|
||||
// const lr = await this.cache.wrap(`${this.name}-historical-lastReload`, () => createHistoricalDefaults(), {ttl: 0}) as object;
|
||||
// const rehydratedLr: any = {};
|
||||
// for(const [k, v] of Object.entries(lr)) {
|
||||
// if(Array.isArray(v)) {
|
||||
// rehydratedLr[k] = new Map(v);
|
||||
// } else {
|
||||
// rehydratedLr[k] = v;
|
||||
// }
|
||||
// }
|
||||
// this.stats.historical.lastReload = rehydratedLr;
|
||||
}
|
||||
|
||||
updateHistoricalStats(data: HistoricalStatUpdateData) {
|
||||
for(const [k, v] of Object.entries(data)) {
|
||||
if(this.stats.historical.lastReload[k] !== undefined) {
|
||||
if(typeof v === 'number') {
|
||||
this.stats.historical.lastReload[k] += v;
|
||||
} else if(this.stats.historical.lastReload[k] instanceof Map) {
|
||||
const keys = Array.isArray(v) ? v : [v];
|
||||
for(const key of keys) {
|
||||
this.stats.historical.lastReload[k].set(key, (this.stats.historical.lastReload[k].get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(this.stats.historical.allTime[k] !== undefined) {
|
||||
if(typeof v === 'number') {
|
||||
this.stats.historical.allTime[k] += v;
|
||||
} else if(this.stats.historical.allTime[k] instanceof Map) {
|
||||
const keys = Array.isArray(v) ? v : [v];
|
||||
for(const key of keys) {
|
||||
this.stats.historical.allTime[k].set(key, (this.stats.historical.allTime[k].get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getHistoricalDisplayStats(): SubredditHistoricalStatsDisplay {
|
||||
return {
|
||||
allTime: createHistoricalStatsDisplay(this.stats.historical.allTime),
|
||||
lastReload: createHistoricalStatsDisplay(this.stats.historical.lastReload)
|
||||
}
|
||||
}
|
||||
|
||||
async saveHistoricalStats() {
|
||||
const atSerializable: any = {};
|
||||
for(const [k, v] of Object.entries(this.stats.historical.allTime)) {
|
||||
if(v instanceof Map) {
|
||||
atSerializable[k] = Array.from(v.entries());
|
||||
} else {
|
||||
atSerializable[k] = v;
|
||||
}
|
||||
}
|
||||
await this.cache.set(`${this.name}-historical-allTime`, atSerializable, {ttl: 0});
|
||||
|
||||
// const lrSerializable: any = {};
|
||||
// for(const [k, v] of Object.entries(this.stats.historical.lastReload)) {
|
||||
// if(v instanceof Map) {
|
||||
// lrSerializable[k] = Array.from(v.entries());
|
||||
// } else {
|
||||
// lrSerializable[k] = v;
|
||||
// }
|
||||
// }
|
||||
// await this.cache.set(`${this.name}-historical-lastReload`, lrSerializable, {ttl: 0});
|
||||
}
|
||||
|
||||
setHistoricalSaveInterval() {
|
||||
this.historicalSaveInterval = setInterval((function(self) {
|
||||
return async () => {
|
||||
await self.saveHistoricalStats();
|
||||
}
|
||||
})(this),10000);
|
||||
}
|
||||
|
||||
async getCacheKeyCount() {
|
||||
if (this.cache.store.keys !== undefined) {
|
||||
if(this.cacheType === 'redis') {
|
||||
@@ -791,7 +897,7 @@ export class BotResourcesManager {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set(subName: string, initOptions: SubredditResourceConfig): SubredditResources {
|
||||
async set(subName: string, initOptions: SubredditResourceConfig): Promise<SubredditResources> {
|
||||
let hash = 'default';
|
||||
const { caching, ...init } = initOptions;
|
||||
|
||||
@@ -841,6 +947,8 @@ export class BotResourcesManager {
|
||||
res.cache.reset();
|
||||
}
|
||||
resource = new SubredditResources(subName, opts);
|
||||
await resource.initHistoricalStats();
|
||||
resource.setHistoricalSaveInterval();
|
||||
this.resources.set(subName, resource);
|
||||
} else {
|
||||
// just set non-cache related settings
|
||||
@@ -851,6 +959,7 @@ export class BotResourcesManager {
|
||||
// reset cache stats when configuration is reloaded
|
||||
resource.stats.cache = cacheStats();
|
||||
}
|
||||
resource.stats.historical.lastReload = createHistoricalDefaults();
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {BotStats, BotStatusResponse, SubredditDataResponse} from "./interfaces";
|
||||
import {ManagerStats, RunningState} from "../../Subreddit/Manager";
|
||||
import {Invokee, RunState} from "../../Common/interfaces";
|
||||
import {cacheStats} from "../../util";
|
||||
import {RunningState} from "../../Subreddit/Manager";
|
||||
import {Invokee, ManagerStats, RunState} from "../../Common/interfaces";
|
||||
import {cacheStats, createHistoricalStatsDisplay} from "../../util";
|
||||
import {createHistoricalDefaults, historicalDefaults} from "../../Common/defaults";
|
||||
|
||||
const managerStats: ManagerStats = {
|
||||
actionsRun: 0,
|
||||
actionsRunSinceStart: 0,
|
||||
actionsRunSinceStartTotal: 0,
|
||||
actionsRunTotal: 0,
|
||||
cache: {
|
||||
currentKeyCount: 0,
|
||||
isShared: false,
|
||||
@@ -18,22 +15,12 @@ const managerStats: ManagerStats = {
|
||||
totalRequests: 0,
|
||||
types: cacheStats()
|
||||
},
|
||||
checksRunSinceStartTotal: 0,
|
||||
checksRunTotal: 0,
|
||||
checksTriggered: 0,
|
||||
checksTriggeredSinceStart: 0,
|
||||
checksTriggeredSinceStartTotal: 0,
|
||||
checksTriggeredTotal: 0,
|
||||
historical: {
|
||||
lastReload: createHistoricalStatsDisplay(createHistoricalDefaults()),
|
||||
allTime: createHistoricalStatsDisplay(createHistoricalDefaults()),
|
||||
},
|
||||
eventsAvg: 0,
|
||||
eventsCheckedSinceStartTotal: 0,
|
||||
eventsCheckedTotal: 0,
|
||||
rulesAvg: 0,
|
||||
rulesCachedSinceStartTotal: 0,
|
||||
rulesCachedTotal: 0,
|
||||
rulesRunSinceStartTotal: 0,
|
||||
rulesRunTotal: 0,
|
||||
rulesTriggeredSinceStartTotal: 0,
|
||||
rulesTriggeredTotal: 0,
|
||||
};
|
||||
const botStats: BotStats = {
|
||||
apiAvg: '-',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {ManagerStats, RunningState} from "../../Subreddit/Manager";
|
||||
import {RunningState} from "../../Subreddit/Manager";
|
||||
import {ManagerStats} from "../../Common/interfaces";
|
||||
|
||||
export interface BotStats {
|
||||
startedAtHuman: string,
|
||||
|
||||
@@ -160,13 +160,19 @@ const status = () => {
|
||||
submissions: acc.checks.submissions + curr.checks.submissions,
|
||||
comments: acc.checks.comments + curr.checks.comments,
|
||||
},
|
||||
eventsCheckedTotal: acc.eventsCheckedTotal + curr.stats.eventsCheckedTotal,
|
||||
checksRunTotal: acc.checksRunTotal + curr.stats.checksRunTotal,
|
||||
checksTriggeredTotal: acc.checksTriggeredTotal + curr.stats.checksTriggeredTotal,
|
||||
rulesRunTotal: acc.rulesRunTotal + curr.stats.rulesRunTotal,
|
||||
rulesCachedTotal: acc.rulesCachedTotal + curr.stats.rulesCachedTotal,
|
||||
rulesTriggeredTotal: acc.rulesTriggeredTotal + curr.stats.rulesTriggeredTotal,
|
||||
actionsRunTotal: acc.actionsRunTotal + curr.stats.actionsRunTotal,
|
||||
historical: {
|
||||
allTime: {
|
||||
eventsCheckedTotal: acc.historical.allTime.eventsCheckedTotal + curr.stats.historical.allTime.eventsCheckedTotal,
|
||||
eventsActionedTotal: acc.historical.allTime.eventsActionedTotal + curr.stats.historical.allTime.eventsActionedTotal,
|
||||
checksRunTotal: acc.historical.allTime.checksRunTotal + curr.stats.historical.allTime.checksRunTotal,
|
||||
checksFromCacheTotal: acc.historical.allTime.checksFromCacheTotal + curr.stats.historical.allTime.checksFromCacheTotal,
|
||||
checksTriggeredTotal: acc.historical.allTime.checksTriggeredTotal + curr.stats.historical.allTime.checksTriggeredTotal,
|
||||
rulesRunTotal: acc.historical.allTime.rulesRunTotal + curr.stats.historical.allTime.rulesRunTotal,
|
||||
rulesCachedTotal: acc.historical.allTime.rulesCachedTotal + curr.stats.historical.allTime.rulesCachedTotal,
|
||||
rulesTriggeredTotal: acc.historical.allTime.rulesTriggeredTotal + curr.stats.historical.allTime.rulesTriggeredTotal,
|
||||
actionsRunTotal: acc.historical.allTime.actionsRunTotal + curr.stats.historical.allTime.actionsRunTotal,
|
||||
}
|
||||
},
|
||||
maxWorkers: acc.maxWorkers + curr.maxWorkers,
|
||||
subMaxWorkers: acc.subMaxWorkers + curr.subMaxWorkers,
|
||||
globalMaxWorkers: acc.globalMaxWorkers + curr.globalMaxWorkers,
|
||||
@@ -178,13 +184,19 @@ const status = () => {
|
||||
submissions: 0,
|
||||
comments: 0,
|
||||
},
|
||||
eventsCheckedTotal: 0,
|
||||
checksRunTotal: 0,
|
||||
checksTriggeredTotal: 0,
|
||||
rulesRunTotal: 0,
|
||||
rulesCachedTotal: 0,
|
||||
rulesTriggeredTotal: 0,
|
||||
actionsRunTotal: 0,
|
||||
historical: {
|
||||
allTime: {
|
||||
eventsCheckedTotal: 0,
|
||||
eventsActionedTotal: 0,
|
||||
checksRunTotal: 0,
|
||||
checksFromCacheTotal: 0,
|
||||
checksTriggeredTotal: 0,
|
||||
rulesRunTotal: 0,
|
||||
rulesCachedTotal: 0,
|
||||
rulesTriggeredTotal: 0,
|
||||
actionsRunTotal: 0,
|
||||
}
|
||||
},
|
||||
maxWorkers: 0,
|
||||
subMaxWorkers: 0,
|
||||
globalMaxWorkers: 0,
|
||||
|
||||
@@ -333,64 +333,63 @@
|
||||
<% } %>
|
||||
<% if (data.name !== 'All') { %>
|
||||
<div data-subreddit="<%= data.name %>"
|
||||
class="stats botStats reloadStats">
|
||||
class="stats botStats reloadStats mb-2">
|
||||
<label>Events</label>
|
||||
<span>
|
||||
<%= data.stats.eventsCheckedSinceStartTotal === undefined ? '-' : data.stats.eventsCheckedSinceStartTotal %>
|
||||
<%= data.stats.historical.lastReload.eventsCheckedTotal === undefined ? '-' : data.stats.historical.lastReload.eventsCheckedTotal %>
|
||||
</span>
|
||||
<label>Checks</label>
|
||||
<span class="has-tooltip">
|
||||
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black -mt-2'>
|
||||
<span><%= data.stats.checksTriggeredSinceStartTotal %></span> Triggered / <span><%= data.stats.checksRunSinceStartTotal %></span> Run
|
||||
<span><%= data.stats.historical.lastReload.checksTriggeredTotal %></span> Triggered / <span><%= data.stats.historical.lastReload.checksRunTotal %></span> Run / <span><%= data.stats.historical.lastReload.checksFromCacheTotal %></span> Cached
|
||||
</span>
|
||||
<% if (data.name !== 'All') { %>
|
||||
<a target="_blank" href="/events?instance=<%= instanceId %>&bot=<%= bot.system.name %>&subreddit=<%= data.name %>" class="underline" style="text-decoration-style: dotted"><%= data.stats.checksTriggeredSinceStartTotal %> T</a>
|
||||
<% } else { %>
|
||||
<%= data.stats.checksTriggeredSinceStartTotal %> T
|
||||
<% } %>/ <span><%= data.stats.checksRunSinceStartTotal %></span> R
|
||||
<span class="cursor-help underline" style="text-decoration-style: dotted"><%= data.stats.historical.lastReload.checksTriggeredTotal %> Triggered</span>
|
||||
</span>
|
||||
|
||||
<label>Rules</label>
|
||||
<span class="has-tooltip">
|
||||
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black -mt-2'>
|
||||
<span><%= data.stats.rulesTriggeredSinceStartTotal %></span> Triggered / <span><%= data.stats.rulesCachedSinceStartTotal %></span> Cached / <span><%= data.stats.rulesRunSinceStartTotal %></span> Run
|
||||
<span><%= data.stats.historical.lastReload.rulesTriggeredTotal %></span> Triggered / <span><%= data.stats.historical.lastReload.rulesCachedTotal %></span> Cached / <span><%= data.stats.historical.lastReload.rulesRunTotal %></span> Run
|
||||
</span>
|
||||
<span class="cursor-help">
|
||||
<span><%= data.stats.rulesTriggeredSinceStartTotal %></span> T / <span><%= data.stats.rulesCachedSinceStartTotal %></span> C / <span><%= data.stats.rulesRunSinceStartTotal %></span> R</span>
|
||||
<span class="cursor-help cursor-help underline" style="text-decoration-style: dotted">
|
||||
<span><%= data.stats.historical.lastReload.rulesTriggeredTotal %></span> Triggered</span>
|
||||
</span>
|
||||
|
||||
<label>Actions</label>
|
||||
<span class="cursor-help"><%= data.stats.actionsRunSinceStartTotal === undefined ? '-' : data.stats.actionsRunSinceStartTotal %></span>
|
||||
<span><%= data.stats.historical.lastReload.actionsRunTotal === undefined ? '0' : data.stats.historical.lastReload.actionsRunTotal %> Run</span>
|
||||
</div>
|
||||
<% } %>
|
||||
<div data-subreddit="<%= data.name %>" class="stats botStats allStats">
|
||||
<div data-subreddit="<%= data.name %>" class="stats botStats allStats mb-2">
|
||||
<label>Events</label>
|
||||
<span>
|
||||
<%= data.stats.eventsCheckedTotal %>
|
||||
<%= data.stats.historical.allTime.eventsCheckedTotal %>
|
||||
</span>
|
||||
<label>Checks</label>
|
||||
<span class="has-tooltip">
|
||||
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black -mt-2'>
|
||||
<span><%= data.stats.checksTriggeredTotal %></span> Triggered / <span><%= data.stats.checksRunTotal %></span> Run
|
||||
<span><%= data.stats.historical.allTime.checksTriggeredTotal %></span> Triggered / <span><%= data.stats.historical.allTime.checksRunTotal %></span> Run / <span><%= data.stats.historical.allTime.checksFromCacheTotal %></span> Cached
|
||||
</span>
|
||||
<span><%= data.stats.checksTriggeredTotal %> T / <span><%= data.stats.checksRunTotal %></span> R</span>
|
||||
<span class="cursor-help underline" style="text-decoration-style: dotted"><%= data.stats.historical.allTime.checksTriggeredTotal %> Triggered</span>
|
||||
</span>
|
||||
|
||||
<label>Rules</label>
|
||||
<span class="has-tooltip">
|
||||
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black -mt-2'>
|
||||
<span><%= data.stats.rulesTriggeredTotal %></span> Triggered / <span><%= data.stats.rulesCachedTotal %></span> Cached / <span><%= data.stats.rulesRunTotal %></span> Run
|
||||
<span><%= data.stats.historical.allTime.rulesTriggeredTotal %></span> Triggered / <span><%= data.stats.historical.allTime.rulesCachedTotal %></span> Cached / <span><%= data.stats.historical.allTime.rulesRunTotal %></span> Run
|
||||
</span>
|
||||
<span class="cursor-help"><span><%= data.stats.rulesTriggeredTotal %></span> T / <span><%= data.stats.rulesCachedTotal %></span> C / <span><%= data.stats.rulesRunTotal %></span> R</span>
|
||||
<span class="cursor-help underline" style="text-decoration-style: dotted"><span><%= data.stats.historical.allTime.rulesTriggeredTotal %></span> Triggered</span>
|
||||
</span>
|
||||
|
||||
<label>Actions</label>
|
||||
<% if (data.name !== 'All') { %>
|
||||
<a target="_blank" href="/events?instance=<%= instanceId %>&bot=<%= bot.system.name %>&subreddit=<%= data.name %>" class="underline" style="text-decoration-style: dotted"><%= data.stats.actionsRunTotal %> Run</a>
|
||||
<% } else { %>
|
||||
<a target="_blank" href="/events?instance=<%= instanceId %>&bot=<%= bot.system.name %>" class="underline" style="text-decoration-style: dotted"><%= data.stats.actionsRunTotal %> Run</a>
|
||||
<% } %>
|
||||
<span>
|
||||
<span><%= data.stats.historical.allTime.actionsRunTotal === undefined ? '0' : data.stats.historical.allTime.actionsRunTotal %> Run</span>
|
||||
</span>
|
||||
</div>
|
||||
<% if (data.name !== 'All') { %>
|
||||
<a target="_blank" href="/events?instance=<%= instanceId %>&bot=<%= bot.system.name %>&subreddit=<%= data.name %>" style="text-decoration-style: dotted">Actioned Events</a>
|
||||
<% } else { %>
|
||||
<a target="_blank" href="/events?instance=<%= instanceId %>&bot=<%= bot.system.name %>">Actioned Events</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-left pb-2">
|
||||
|
||||
85
src/util.ts
85
src/util.ts
@@ -9,11 +9,12 @@ import {InvalidOptionArgumentError} from "commander";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import {Comment} from "snoowrap";
|
||||
import {inflateSync, deflateSync} from "zlib";
|
||||
import sizeOf from 'image-size';
|
||||
import {
|
||||
ActivityWindowCriteria, CacheOptions, CacheProvider,
|
||||
DurationComparison,
|
||||
GenericComparison, LogInfo, NamedGroup,
|
||||
PollingOptionsStrong, RedditEntity, RedditEntityType, RegExResult, ResourceStats, StatusCodeError,
|
||||
GenericComparison, HistoricalStats, HistoricalStatsDisplay, ImageData, ImageDetection, LogInfo, NamedGroup,
|
||||
PollingOptionsStrong, RedditEntity, RedditEntityType, RegExResult, ResembleResult, ResourceStats, StatusCodeError,
|
||||
StringOperator, StrongSubredditState, SubredditState
|
||||
} from "./Common/interfaces";
|
||||
import JSON5 from "json5";
|
||||
@@ -30,6 +31,7 @@ import {create as createMemoryStore} from './Utils/memoryStore';
|
||||
import {MESSAGE} from "triple-beam";
|
||||
import {RedditUser} from "snoowrap/dist/objects";
|
||||
import reRegExp from '@stdlib/regexp-regexp';
|
||||
import fetch, {Response} from "node-fetch";
|
||||
|
||||
const ReReg = reRegExp();
|
||||
|
||||
@@ -841,8 +843,8 @@ export const boolToString = (val: boolean): string => {
|
||||
return val ? 'Yes' : 'No';
|
||||
}
|
||||
|
||||
export const isRedditMedia = (act: Submission): boolean => {
|
||||
return act.is_reddit_media_domain || act.is_video || ['v.redd.it','i.redd.it'].includes(act.domain);
|
||||
export const isRedditMedia = (act: Comment | Submission): boolean => {
|
||||
return asSubmission(act) && (act.is_reddit_media_domain || act.is_video || ['v.redd.it','i.redd.it'].includes(act.domain));
|
||||
}
|
||||
|
||||
export const isExternalUrlSubmission = (act: Comment | Submission): boolean => {
|
||||
@@ -1158,3 +1160,78 @@ export const parseRuleResultsToMarkdownSummary = (ruleResults: RuleResult[]): st
|
||||
});
|
||||
return results.join('\r\n');
|
||||
}
|
||||
|
||||
export const isValidImageURL = (str: string): boolean => {
|
||||
return !!str.match(/\w+\.(jpg|jpeg|gif|png|tiff|bmp)$/gi);
|
||||
}
|
||||
|
||||
export const getImageDataFromUrl = async (url: string, aggressive = false): Promise<[Response?, ImageData?, string?]> => {
|
||||
if (!aggressive && !isValidImageURL(url)) {
|
||||
return [undefined, undefined, 'URL did not end with a valid image extension'];
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const ct = response.headers.get('Content-Type');
|
||||
if (ct !== null && ct.includes('image')) {
|
||||
let buffer = await response.buffer();
|
||||
const dimensions = sizeOf(buffer);
|
||||
return [response, {
|
||||
data: buffer,
|
||||
width: dimensions.width as number - 5,
|
||||
height: dimensions.height as number - 5,
|
||||
pixels: (dimensions.height as number - 5) * (dimensions.width as number - 5)
|
||||
}];
|
||||
}
|
||||
return [response, undefined, 'Content-Type for fetched URL did not contain "image"'];
|
||||
}
|
||||
return [response, undefined, `URL response was not OK: (${response.status})${response.statusText}`];
|
||||
} catch (err) {
|
||||
return [undefined, undefined, `Error occurred while fetching response from URL: ${err.message}`];
|
||||
}
|
||||
}
|
||||
|
||||
let resembleCIFunc: Function;
|
||||
|
||||
const getCIFunc = async () => {
|
||||
if (resembleCIFunc === undefined) {
|
||||
// @ts-ignore
|
||||
const resembleModule = await import('resemblejs/compareImages');
|
||||
if (resembleModule === undefined) {
|
||||
throw new Error('Could not import resemblejs');
|
||||
}
|
||||
resembleCIFunc = resembleModule.default;
|
||||
}
|
||||
return resembleCIFunc;
|
||||
}
|
||||
|
||||
export const compareImages = async (data1: ImageData, data2: ImageData, threshold?: number): Promise<[ResembleResult, boolean?]> => {
|
||||
let ci: Function;
|
||||
|
||||
try {
|
||||
ci = await getCIFunc();
|
||||
} catch (err) {
|
||||
err.message = `Unable to do image comparison due to an issue importing the comparison library. It is likely 'node-canvas' is not installed (see ContextMod docs). Error Message: ${err.message}`;
|
||||
throw err;
|
||||
}
|
||||
const results = await ci(data1.data, data2.data, {
|
||||
returnEarlyThreshold: threshold !== undefined ? Math.min(threshold + 5, 100) : undefined,
|
||||
}) as ResembleResult;
|
||||
|
||||
const sameImage = threshold === undefined ? undefined : results.rawMisMatchPercentage < threshold;
|
||||
return [results, sameImage];
|
||||
}
|
||||
|
||||
export const createHistoricalStatsDisplay = (data: HistoricalStats): HistoricalStatsDisplay => {
|
||||
const display: any = {};
|
||||
for(const [k, v] of Object.entries(data)) {
|
||||
if(v instanceof Map) {
|
||||
display[k] = v;
|
||||
display[`${k}Total`] = Array.from(v.values()).reduce((acc, curr) => acc + curr, 0);
|
||||
} else {
|
||||
display[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
return display as HistoricalStatsDisplay;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user