Compare commits

...

19 Commits

Author SHA1 Message Date
FoxxMD
39daa11f2d Merge branch 'edge' 2021-11-15 12:53:28 -05:00
FoxxMD
93de38a845 fix(filter): Missing 'op' property test on itemIs for comment 2021-11-15 12:47:34 -05:00
FoxxMD
43caaca1f2 fix(config): Fix wrong provider data used when bot has no caching config
Use previously built defaultProvider, instead of hardcoded memory config, so any cache config from operator waterfalls to bot/subreddit

Closes #46
2021-11-02 15:55:03 -04:00
FoxxMD
7bcc0195fe fix(author): change include/exclude on RULE to be optional
One of these properties must be present for the rule to be valid. This is enforced at runtime. Change the schema so that both are optional from the config-side though.
2021-11-02 09:41:55 -04:00
FoxxMD
dac6541e28 Merge branch 'edge' 2021-11-01 16:12:43 -04:00
FoxxMD
2504a34a34 refactor(docker): Remove old steps from node-canvas dep
Not using node-canvas anymore so dockerfile can be simplified #48
2021-11-01 15:55:12 -04:00
FoxxMD
e19639ad0d fix(ui): Forgot to add tailwin css to git
Oops!
2021-11-01 15:06:50 -04:00
FoxxMD
b8084e02b5 Merge branch 'colorImprovements' into edge 2021-11-01 14:58:01 -04:00
FoxxMD
97906281e6 Merge branch 'edge' 2021-11-01 14:55:10 -04:00
FoxxMD
2cea119657 feat(ui): Add tooltip descriptions for depleted/limit reset stats
Closes #36
2021-11-01 14:52:35 -04:00
FoxxMD
6f16d289dd feat(ui): Add lang attribute for html
Closes #35
2021-11-01 14:22:20 -04:00
FoxxMD
a96575c6b3 refactor(ui): Use local tailwind css asset
Closes #34
2021-11-01 14:19:47 -04:00
FoxxMD
0a82e83352 refactor(ui)!: Implement colorbind color changes for rest of UI and remove dark/light mode
* Apply changes made in ac409dce to the rest of the application
* Remove dark/light mode -- now always dark mode (easier to maintain this way)
* Remove dependency on tailwind dark css
2021-11-01 14:14:49 -04:00
FoxxMD
d5e1cdec61 fix(criteria): Improve criteria filtering for removed/deleted activities and logging
* Use different logging messages when criteria is not available due to mod permissions (property not available to non-mods)
* Change logging level for missing/unavailable criteria to reduce logging noise. On unavailable use debug, on missing use warn
* Improve activity removed/deleted detection based on whether activity is moddable by current user
2021-11-01 13:25:54 -04:00
FoxxMD
ef40c25b09 feat(attribution): Add additional subreddit and activity filtering functionality to criteria
* Refactor subreddit filtering with include/exclude to use subreddit state
* Add submissionState and commentState filters
2021-11-01 11:26:55 -04:00
FoxxMD
6370a2976a Merge branch 'edge' into colorImprovements 2021-11-01 10:23:40 -04:00
FoxxMD
d8180299ea fix(author): Fix missing true return statement for author flair check 2021-11-01 10:23:22 -04:00
FoxxMD
ac409dce3d POC color improvements for color blindness
Needs another pass and consolidate/clean up for pages other than status
2021-10-20 21:12:31 -04:00
FoxxMD
56c007c20d feat(author): Implement author profile description regex/string testing
May test "description" in authorIs as a regular express of string literal -- or as an array of the aforementioned values
2021-10-20 19:52:46 -04:00
33 changed files with 512 additions and 333 deletions

View File

@@ -1,14 +1,10 @@
FROM node:16-alpine3.12
FROM node:16-alpine3.14
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 vips
# vips required to run sharp library for image comparison
RUN echo "http://dl-4.alpinelinux.org/alpine/v3.14/community" >> /etc/apk/repositories \
&& apk --update add vips
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
@@ -17,9 +13,7 @@ WORKDIR /usr/app
COPY package*.json ./
COPY tsconfig.json .
# 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
RUN npm install
ADD . /usr/app

View File

@@ -1,5 +1,6 @@
import {UserNoteCriteria} from "../Rule";
import {CompareValue, CompareValueOrPercent, DurationComparor} from "../Common/interfaces";
import {parseStringToRegex} from "../util";
/**
* If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.
@@ -106,6 +107,17 @@ export interface AuthorCriteria {
* This is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned
* */
shadowBanned?: boolean
/**
* An (array of) string/regular expression to test contents of an Author's profile description against
*
* If no flags are specified then the **insensitive** flag is used by default
*
* If using an array then if **any** value in the array passes the description test passes
*
* @examples [["/test$/i", "look for this string literal"]]
* */
description?: string | string[]
}
export class Author implements AuthorCriteria {
@@ -120,6 +132,7 @@ export class Author implements AuthorCriteria {
totalKarma?: string;
verified?: boolean;
shadowBanned?: boolean;
description?: string[];
constructor(options: AuthorCriteria) {
this.name = options.name;
@@ -132,6 +145,7 @@ export class Author implements AuthorCriteria {
this.linkKarma = options.linkKarma;
this.totalKarma = options.totalKarma;
this.shadowBanned = options.shadowBanned;
this.description = options.description === undefined ? undefined : Array.isArray(options.description) ? options.description : [options.description];
}
}

View File

@@ -932,6 +932,9 @@ export interface SubmissionState extends ActivityState {
link_flair_css_class?: string
}
// properties calculated/derived by CM -- not provided as plain values by reddit
export const cmActivityProperties = ['submissionState','score','reports','removed','deleted','filtered','age','title'];
/**
* Different attributes a `Comment` can be in. Only include a property if you want to check it.
* @examples [{"op": true, "removed": false}]

View File

@@ -656,10 +656,7 @@ export const buildOperatorConfigWithDefaults = (data: OperatorJsonConfig): Opera
...cacheTTLDefaults,
actionedEventsDefault: opActionedEventsDefault,
actionedEventsMax: opActionedEventsMax,
provider: {
store: 'memory',
...cacheOptDefaults
}
provider: {...defaultProvider}
};
} else {
const {

View File

@@ -1,12 +1,12 @@
import {SubmissionRule, SubmissionRuleJSONConfig} from "./SubmissionRule";
import {ActivityWindowType, DomainInfo, ReferenceSubmission} from "../Common/interfaces";
import {ActivityWindowType, CommentState, DomainInfo, ReferenceSubmission, SubmissionState} from "../Common/interfaces";
import {Rule, RuleOptions, RuleResult} from "./index";
import Submission from "snoowrap/dist/objects/Submission";
import {getAttributionIdentifier} from "../Utils/SnoowrapUtils";
import dayjs from "dayjs";
import {
asSubmission,
comparisonTextOp,
comparisonTextOp, convertSubredditsRawToStrong,
FAIL,
formatNumber, getActivitySubredditName, isSubmission,
parseGenericValueOrPercentComparison,
@@ -15,6 +15,7 @@ import {
} from "../util";
import { Comment } from "snoowrap/dist/objects";
import SimpleError from "../Utils/SimpleError";
import as from "async";
export interface AttributionCriteria {
@@ -76,25 +77,41 @@ export interface AttributionCriteria {
domainsCombined?: boolean,
/**
* Only include Activities from this list of Subreddits (by name, case-insensitive)
* When present, Activities WILL ONLY be counted if they are found in this list of Subreddits
*
* Each value in the list can be either:
*
* EX `["mealtimevideos","askscience"]`
* @examples ["mealtimevideos","askscience"]
* @minItems 1
* * string (name of subreddit)
* * regular expression to run on the subreddit name
* * `SubredditState`
*
* EX `["mealtimevideos","askscience", "/onlyfans*\/i", {"over18": true}]`
* @examples [["mealtimevideos","askscience", "/onlyfans*\/i", {"over18": true}]]
* */
include?: string[],
/**
* Do not include Activities from this list of Subreddits (by name, case-insensitive)
* When present, Activities WILL NOT be counted if they are found in this list of Subreddits
*
* Will be ignored if `include` is present.
* Each value in the list can be either:
*
* EX `["mealtimevideos","askscience"]`
* @examples ["mealtimevideos","askscience"]
* @minItems 1
* * string (name of subreddit)
* * regular expression to run on the subreddit name
* * `SubredditState`
*
* EX `["mealtimevideos","askscience", "/onlyfans*\/i", {"over18": true}]`
* @examples [["mealtimevideos","askscience", "/onlyfans*\/i", {"over18": true}]]
* */
exclude?: string[],
/**
* When present, Submissions from `window` will only be counted if they meet this criteria
* */
submissionState?: SubmissionState
/**
* When present, Comments from `window` will only be counted if they meet this criteria
* */
commentState?: CommentState
/**
* This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`
*
@@ -178,21 +195,36 @@ export class AttributionRule extends Rule {
consolidateMediaDomains = false,
domains = [],
domainsCombined = false,
include: includeRaw = [],
exclude: excludeRaw = [],
include = [],
exclude = [],
commentState,
submissionState,
} = criteria;
const include = includeRaw.map(x => parseSubredditName(x).toLowerCase());
const exclude = excludeRaw.map(x => parseSubredditName(x).toLowerCase());
const {operator, value, isPercent, extra = ''} = parseGenericValueOrPercentComparison(threshold);
let activities = thresholdOn === 'submissions' ? await this.resources.getAuthorSubmissions(item.author, {window: window}) : await this.resources.getAuthorActivities(item.author, {window: window});
activities = activities.filter(act => {
if (include.length > 0) {
return include.some(x => x === getActivitySubredditName(act).toLowerCase());
} else if (exclude.length > 0) {
return !exclude.some(x => x === getActivitySubredditName(act).toLowerCase())
if(include.length > 0 || exclude.length > 0) {
const defaultOpts = {
defaultFlags: 'i',
generateDescription: true
};
if(include.length > 0) {
const subStates = include.map(x => convertSubredditsRawToStrong(x, defaultOpts));
activities = await this.resources.batchTestSubredditCriteria(activities, subStates);
} else {
const subStates = exclude.map(x => convertSubredditsRawToStrong(x, defaultOpts));
const toExclude = (await this.resources.batchTestSubredditCriteria(activities, subStates)).map(x => x.id);
activities = activities.filter(x => !toExclude.includes(x.id));
}
}
activities = await as.filter(activities, async (activity) => {
if (asSubmission(activity) && submissionState !== undefined) {
return await this.resources.testItemCriteria(activity, [submissionState]);
} else if (commentState !== undefined) {
return await this.resources.testItemCriteria(activity, [commentState]);
}
return true;
});

View File

@@ -12,11 +12,11 @@ export interface AuthorRuleConfig {
/**
* Will "pass" if any set of AuthorCriteria passes
* */
include: AuthorCriteria[];
include?: AuthorCriteria[];
/**
* Only runs if include is not present. Will "pass" if any of set of the AuthorCriteria does not pass
* */
exclude: AuthorCriteria[];
exclude?: AuthorCriteria[];
}
export interface AuthorRuleOptions extends AuthorRuleConfig, RuleOptions {
@@ -34,8 +34,13 @@ export class AuthorRule extends Rule {
constructor(options: AuthorRuleOptions) {
super(options);
this.include = options.include.map(x => new Author(x));
this.exclude = options.exclude.map(x => new Author(x));
const {
include,
exclude,
} = options;
this.include = include !== undefined ? include.map(x => new Author(x)) : [];
this.exclude = exclude !== undefined ? exclude.map(x => new Author(x)) : [];
if(this.include.length === 0 && this.exclude.length === 0) {
throw new Error('At least one of the properties [include,exclude] on Author Rule must not be empty');

View File

@@ -10,7 +10,7 @@ import {
asSubmission, bitsToHexLength,
// blockHashImage,
compareImages,
comparisonTextOp,
comparisonTextOp, convertSubredditsRawToStrong,
FAIL,
formatNumber,
getActivitySubredditName, imageCompareMaxConcurrencyGuess,
@@ -284,15 +284,11 @@ export class RecentActivityRule extends Rule {
} = triggerSet;
// convert subreddits array into entirely StrongSubredditState
const subStates: StrongSubredditState[] = subreddits.map((x) => {
if (typeof x === 'string') {
return toStrongSubredditState({name: x, stateDescription: x}, {
defaultFlags: 'i',
generateDescription: true
});
}
return toStrongSubredditState(x, {defaultFlags: 'i', generateDescription: true});
});
const defaultOpts = {
defaultFlags: 'i',
generateDescription: true
};
const subStates: StrongSubredditState[] = subreddits.map((x) => convertSubredditsRawToStrong(x, defaultOpts));
let validActivity: (Comment | Submission)[] = await as.filter(viableActivity, async (activity) => {
if (asSubmission(activity) && submissionState !== undefined) {

View File

@@ -29,6 +29,26 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"description": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "An (array of) string/regular expression to test contents of an Author's profile description against\n\nIf no flags are specified then the **insensitive** flag is used by default\n\nIf using an array then if **any** value in the array passes the description test passes",
"examples": [
[
"/test$/i",
"look for this string literal"
]
]
},
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"examples": [

View File

@@ -249,6 +249,16 @@
},
"type": "array"
},
"commentState": {
"$ref": "#/definitions/CommentState",
"description": "When present, Comments from `window` will only be counted if they meet this criteria",
"examples": [
{
"op": true,
"removed": false
}
]
},
"consolidateMediaDomains": {
"default": false,
"description": "Should the criteria consolidate recognized media domains into the parent domain?\n\nSubmissions to major media domains (youtube, vimeo) can be identified by individual Channel/Author...\n\n* If `false` then domains will be aggregated at the channel level IE Youtube Channel A (2 counts), Youtube Channel B (3 counts)\n* If `true` then then media domains will be consolidated at domain level and then aggregated IE youtube.com (5 counts)",
@@ -277,27 +287,37 @@
"type": "boolean"
},
"exclude": {
"description": "Do not include Activities from this list of Subreddits (by name, case-insensitive)\n\nWill be ignored if `include` is present.\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL NOT be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"include": {
"description": "Only include Activities from this list of Subreddits (by name, case-insensitive)\n\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL ONLY be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"minActivityCount": {
@@ -308,6 +328,16 @@
"name": {
"type": "string"
},
"submissionState": {
"$ref": "#/definitions/SubmissionState",
"description": "When present, Submissions from `window` will only be counted if they meet this criteria",
"examples": [
{
"over_18": true,
"removed": false
}
]
},
"threshold": {
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
@@ -454,6 +484,26 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"description": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "An (array of) string/regular expression to test contents of an Author's profile description against\n\nIf no flags are specified then the **insensitive** flag is used by default\n\nIf using an array then if **any** value in the array passes the description test passes",
"examples": [
[
"/test$/i",
"look for this string literal"
]
]
},
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"examples": [
@@ -621,8 +671,6 @@
}
},
"required": [
"exclude",
"include",
"kind"
],
"type": "object"

View File

@@ -195,6 +195,16 @@
},
"type": "array"
},
"commentState": {
"$ref": "#/definitions/CommentState",
"description": "When present, Comments from `window` will only be counted if they meet this criteria",
"examples": [
{
"op": true,
"removed": false
}
]
},
"consolidateMediaDomains": {
"default": false,
"description": "Should the criteria consolidate recognized media domains into the parent domain?\n\nSubmissions to major media domains (youtube, vimeo) can be identified by individual Channel/Author...\n\n* If `false` then domains will be aggregated at the channel level IE Youtube Channel A (2 counts), Youtube Channel B (3 counts)\n* If `true` then then media domains will be consolidated at domain level and then aggregated IE youtube.com (5 counts)",
@@ -223,27 +233,37 @@
"type": "boolean"
},
"exclude": {
"description": "Do not include Activities from this list of Subreddits (by name, case-insensitive)\n\nWill be ignored if `include` is present.\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL NOT be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"include": {
"description": "Only include Activities from this list of Subreddits (by name, case-insensitive)\n\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL ONLY be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"minActivityCount": {
@@ -254,6 +274,16 @@
"name": {
"type": "string"
},
"submissionState": {
"$ref": "#/definitions/SubmissionState",
"description": "When present, Submissions from `window` will only be counted if they meet this criteria",
"examples": [
{
"over_18": true,
"removed": false
}
]
},
"threshold": {
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
@@ -400,6 +430,26 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"description": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "An (array of) string/regular expression to test contents of an Author's profile description against\n\nIf no flags are specified then the **insensitive** flag is used by default\n\nIf using an array then if **any** value in the array passes the description test passes",
"examples": [
[
"/test$/i",
"look for this string literal"
]
]
},
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"examples": [
@@ -567,8 +617,6 @@
}
},
"required": [
"exclude",
"include",
"kind"
],
"type": "object"

View File

@@ -172,6 +172,16 @@
},
"type": "array"
},
"commentState": {
"$ref": "#/definitions/CommentState",
"description": "When present, Comments from `window` will only be counted if they meet this criteria",
"examples": [
{
"op": true,
"removed": false
}
]
},
"consolidateMediaDomains": {
"default": false,
"description": "Should the criteria consolidate recognized media domains into the parent domain?\n\nSubmissions to major media domains (youtube, vimeo) can be identified by individual Channel/Author...\n\n* If `false` then domains will be aggregated at the channel level IE Youtube Channel A (2 counts), Youtube Channel B (3 counts)\n* If `true` then then media domains will be consolidated at domain level and then aggregated IE youtube.com (5 counts)",
@@ -200,27 +210,37 @@
"type": "boolean"
},
"exclude": {
"description": "Do not include Activities from this list of Subreddits (by name, case-insensitive)\n\nWill be ignored if `include` is present.\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL NOT be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"include": {
"description": "Only include Activities from this list of Subreddits (by name, case-insensitive)\n\n\nEX `[\"mealtimevideos\",\"askscience\"]`",
"description": "When present, Activities WILL ONLY be counted if they are found in this list of Subreddits\n\nEach value in the list can be either:\n\n * string (name of subreddit)\n * regular expression to run on the subreddit name\n * `SubredditState`\n\nEX `[\"mealtimevideos\",\"askscience\", \"/onlyfans*\\/i\", {\"over18\": true}]`",
"examples": [
"mealtimevideos",
"askscience"
[
"mealtimevideos",
"askscience",
"/onlyfans*/i",
{
"over18": true
}
]
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"minActivityCount": {
@@ -231,6 +251,16 @@
"name": {
"type": "string"
},
"submissionState": {
"$ref": "#/definitions/SubmissionState",
"description": "When present, Submissions from `window` will only be counted if they meet this criteria",
"examples": [
{
"over_18": true,
"removed": false
}
]
},
"threshold": {
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
@@ -377,6 +407,26 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"description": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "An (array of) string/regular expression to test contents of an Author's profile description against\n\nIf no flags are specified then the **insensitive** flag is used by default\n\nIf using an array then if **any** value in the array passes the description test passes",
"examples": [
[
"/test$/i",
"look for this string literal"
]
]
},
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"examples": [
@@ -544,8 +594,6 @@
}
},
"required": [
"exclude",
"include",
"kind"
],
"type": "object"

View File

@@ -37,7 +37,10 @@ import {
ActionedEvent,
SubredditState,
StrongSubredditState,
HistoricalStats, HistoricalStatUpdateData, SubredditHistoricalStats, SubredditHistoricalStatsDisplay
HistoricalStats,
HistoricalStatUpdateData,
SubredditHistoricalStats,
SubredditHistoricalStatsDisplay,
} from "../Common/interfaces";
import UserNotes from "./UserNotes";
import Mustache from "mustache";
@@ -818,6 +821,10 @@ export class SubredditResources {
}
break;
case 'reports':
if (!item.can_mod_post) {
log.debug(`Cannot test for reports on Activity in a subreddit bot account is not a moderato Activist. Skipping criteria...`);
break;
}
const reportCompare = parseGenericValueComparison(crit[k] as string);
if(!comparisonTextOp(item.num_reports, reportCompare.operator, reportCompare.value)) {
// @ts-ignore
@@ -842,6 +849,10 @@ export class SubredditResources {
}
break;
case 'filtered':
if (!item.can_mod_post) {
log.debug(`Cannot test for 'filtered' state on Activity in a subreddit bot account is not a moderator for. Skipping criteria...`);
break;
}
const filtered = activityIsFiltered(item);
if (filtered !== crit['filtered']) {
// @ts-ignore
@@ -864,7 +875,7 @@ export class SubredditResources {
// @ts-ignore
const titleReg = crit[k] as string;
try {
if(null === item.title.match(titleReg)) {
if (null === item.title.match(titleReg)) {
// @ts-ignore
log.debug(`Failed to match title as regular expression: ${titleReg}`);
return false;
@@ -874,6 +885,31 @@ export class SubredditResources {
return false
}
break;
case 'approved':
case 'spam':
if(!item.can_mod_post) {
log.debug(`Cannot test for '${k}' state on Activity in a subreddit bot account is not a moderator for. Skipping criteria...`);
break;
}
// @ts-ignore
if (item[k] !== crit[k]) {
// @ts-ignore
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${k}:${item[k]}`)
return false
}
break;
case 'op':
if(item instanceof Submission) {
log.warn(`On a Submission the 'op' property will always be true. Did you mean to use this on a comment instead?`);
break;
}
// @ts-ignore
if (item.is_submitter !== crit[k]) {
// @ts-ignore
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${k}:${item[k]}`)
return false
}
break;
default:
// @ts-ignore
if (item[k] !== undefined) {
@@ -884,7 +920,11 @@ export class SubredditResources {
return false
}
} else {
log.warn(`Tried to test for Item property '${k}' but it did not exist`);
if(!item.can_mod_post) {
log.warn(`Tried to test for Activity property '${k}' but it did not exist. This Activity is not in a subreddit the bot can mod so it may be that this property is only available to mods of that subreddit. Or the property may be misspelled.`);
} else {
log.warn(`Tried to test for Activity property '${k}' but it did not exist. Check the spelling of the property.`);
}
}
break;
}

View File

@@ -14,14 +14,14 @@ import {
} from "../Common/interfaces";
import {
compareDurationValue,
comparisonTextOp, getActivityAuthorName,
comparisonTextOp, escapeRegex, getActivityAuthorName,
isActivityWindowCriteria, isStatusError,
normalizeName,
parseDuration,
parseDurationComparison,
parseGenericValueComparison,
parseGenericValueOrPercentComparison,
parseRuleResultsToMarkdownSummary,
parseRuleResultsToMarkdownSummary, parseStringToRegex,
parseSubredditName,
truncateStringToLength
} from "../util";
@@ -377,7 +377,7 @@ export const testAuthorCriteria = async (item: (Comment | Submission), authorOpt
// @ts-ignore
for (const c of authorOpts[k]) {
if (c === css) {
return;
return true;
}
}
return false;
@@ -393,7 +393,7 @@ export const testAuthorCriteria = async (item: (Comment | Submission), authorOpt
// @ts-ignore
for (const c of authorOpts[k]) {
if (c === text) {
return
return true;
}
}
return false;
@@ -463,6 +463,28 @@ export const testAuthorCriteria = async (item: (Comment | Submission), authorOpt
return false;
}
break;
case 'description':
// @ts-ignore
const desc = await item.author.subreddit?.display_name.public_description;
const dVals = authorOpts[k] as string[];
let passed = false;
for(const val of dVals) {
let reg = parseStringToRegex(val, 'i');
if(reg === undefined) {
reg = parseStringToRegex(`/.*${escapeRegex(val.trim())}.*/`, 'i');
if(reg === undefined) {
throw new SimpleError(`Could not convert 'description' value to a valid regex: ${authorOpts[k] as string}`);
}
}
if(reg.test(desc)) {
passed = true;
break;
}
}
if(!passed) {
return false;
}
break;
case 'userNotes':
const notes = await userNotes.getUserNotes(item.author);
const notePass = () => {
@@ -661,13 +683,21 @@ export const getAttributionIdentifier = (sub: Submission, useParentMediaDomain =
}
export const activityIsRemoved = (item: Submission | Comment): boolean => {
if (item instanceof Submission) {
// when automod filters a post it gets this category
return item.banned_at_utc !== null && item.removed_by_category !== 'automod_filtered';
if(item.can_mod_post) {
if (item instanceof Submission) {
// when automod filters a post it gets this category
return item.banned_at_utc !== null && item.removed_by_category !== 'automod_filtered';
}
// when automod filters a comment item.removed === false
// so if we want to processing filtered comments we need to check for this
return item.banned_at_utc !== null && item.removed;
} else {
if (item instanceof Submission) {
return item.removed_by_category === 'moderator' || item.removed_by_category === 'deleted';
}
// in subreddits the bot does not mod it is not possible to tell the difference between a comment that was removed by the user and one that was removed by a mod
return item.body === '[removed]';
}
// when automod filters a comment item.removed === false
// so if we want to processing filtered comments we need to check for this
return item.banned_at_utc !== null && item.removed;
}
export const activityIsFiltered = (item: Submission | Comment): boolean => {

View File

@@ -4,7 +4,7 @@ a {
.loading {
height: 35px;
fill: black;
fill: white;
display: none;
}
@@ -12,10 +12,6 @@ a {
display: inline;
}
.dark .loading {
fill: white;
}
.sub {
display: none;
}
@@ -91,3 +87,19 @@ a {
pointer-events: initial;
text-decoration: initial;
}
.blue {
color: rgb(97, 175, 239)
}
.red {
color: rgb(255 123 133);
}
.green {
color: rgb(170 255 109);
}
.purple {
color: rgb(224 124 253);
}
.yellow {
color: rgb(253 198 94);
}

View File

@@ -0,0 +1,10 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="q" viewBox="0 0 24 24">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</symbol>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 486 B

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,11 @@
<html>
<html lang="en">
<%- include('partials/head', {title: 'CM OAuth Helper'}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/title', {title: ' OAuth Helper'}) %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-600">
<div class="p-6 md:px-10 md:py-6">
<div class="text-xl mb-4">Congrats! You did the thing.</div>
<div class="space-y-3">

View File

@@ -1,11 +1,6 @@
<html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind.min.css"
integrity="sha512-wl80ucxCRpLkfaCnbM88y4AxnutbGk327762eM9E/rRTvY/ZGAHWMZrYUq66VQBYMIYDFpDdJAOGSLyIPHZ2IQ=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind-dark.min.css"
integrity="sha512-WvyKyiVHgInX5UQt67447ExtRRZG/8GUijaq1MpqTNYp8wY4/EJOG5bI80sRp/5crDy4Z6bBUydZI2OFV3Vbtg=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="/public/tailwind.min.css"/>
<script src="https://code.iconify.design/1/1.0.4/iconify.min.js"></script>
<link rel="stylesheet" href="/public/themeToggle.css">
<link rel="stylesheet" href="/public/app.css">
@@ -15,13 +10,12 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--icons from https://heroicons.com -->
</head>
<body style="user-select: none;" class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body style="user-select: none;" class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen bg-gray-800">
<%- include('partials/title') %>
<div class="container mx-auto">
<div class="grid">
<div class="dark:text-white mb-3 pl-2">
<div class="my-3 pl-2">
Schema <a href="/config?schema=subreddit" id="subredditSchemaType">Subreddit</a> / <a href="/config?schema=operator" id="operatorSchemaType">Operator</a> |
<span class="has-tooltip">
<span style="z-index:999; margin-top: 30px;" class='tooltip rounded shadow-lg p-3 bg-gray-100 text-black space-y-2'>
@@ -63,35 +57,6 @@
</div>
<%- include('partials/footer') %>
</div>
<script>
document.querySelectorAll('.theme').forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
if (e.target.id === 'dark') {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
document.querySelectorAll('.theme').forEach(el => {
el.classList.remove('font-bold', 'no-underline', 'pointer-events-none');
});
e.target.classList.add('font-bold', 'no-underline', 'pointer-events-none');
})
})
document.querySelector("#themeToggle").checked = localStorage.getItem('ms-dark') !== 'no';
document.querySelector("#themeToggle").onchange = (e) => {
if (e.target.checked === true) {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
}
</script>
<script src="/monaco/dev/vs/loader.js"></script>
<script>
require.config({ paths: { vs: 'monaco/dev/vs' } });

View File

@@ -1,12 +1,11 @@
<html>
<html lang="en">
<%- include('partials/head', {title: 'CM'}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/title', {title: 'Error'}) %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-600">
<div class="p-6 md:px-10 md:py-6">
<div class="text-xl mb-4">Oops 😬</div>
<div class="space-y-3">

View File

@@ -1,11 +1,6 @@
<html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind.min.css"
integrity="sha512-wl80ucxCRpLkfaCnbM88y4AxnutbGk327762eM9E/rRTvY/ZGAHWMZrYUq66VQBYMIYDFpDdJAOGSLyIPHZ2IQ=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind-dark.min.css"
integrity="sha512-WvyKyiVHgInX5UQt67447ExtRRZG/8GUijaq1MpqTNYp8wY4/EJOG5bI80sRp/5crDy4Z6bBUydZI2OFV3Vbtg=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="/public/tailwind.min.css"/>
<script src="https://code.iconify.design/1/1.0.4/iconify.min.js"></script>
<link rel="stylesheet" href="/public/themeToggle.css">
<link rel="stylesheet" href="/public/app.css">
@@ -20,19 +15,19 @@
}
</style>
</head>
<body>
<body class="bg-gray-900 text-white font-sans">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/title') %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white px-3 py-6 space-y-3">
<div class="px-3 py-6 space-y-3">
<% if(data.length === 0) { %>
No events have been actioned yet!
<% } %>
<% data.forEach(function (eRes){ %>
<div class="shadow-lg">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 dark:bg-gray-700 dark:text-white">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-700">
<div class="flex items-center justify-between">
<div>
<span class="peek"><%- eRes.activity.peek %></span><a target="_blank" href="https://reddit.com<%= eRes.activity.link%>">(Link)</a>
@@ -42,7 +37,7 @@
</div>
</div>
</div>
<div class="p-4 pl-6 pt-3 space-y-2">
<div class="p-4 pl-6 pt-3 space-y-2 bg-gray-500">
<div><span class="font-semibold">Check:</span> <%= eRes.check %><span class="px-3">&#10132;</span><%= eRes.ruleSummary %></div>
<div>
<span class="font-semibold">Rules:</span>
@@ -67,34 +62,5 @@
</div>
<%- include('partials/footer') %>
</div>
<script>
document.querySelectorAll('.theme').forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
if (e.target.id === 'dark') {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
document.querySelectorAll('.theme').forEach(el => {
el.classList.remove('font-bold', 'no-underline', 'pointer-events-none');
});
e.target.classList.add('font-bold', 'no-underline', 'pointer-events-none');
})
})
document.querySelector("#themeToggle").checked = localStorage.getItem('ms-dark') !== 'no';
document.querySelector("#themeToggle").onchange = (e) => {
if (e.target.checked === true) {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
}
</script>
</body>
</html>

View File

@@ -1,12 +1,11 @@
<html>
<html lang="en">
<%- include('partials/head', {title: 'CM OAuth Helper'}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/title', {title: ' OAuth Helper'}) %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-600">
<div class="p-6 md:px-10 md:py-6">
<div class="text-xl mb-4">Hi! Looks like you're setting up your bot. To get running:</div>
<div class="text-lg text-semibold my-3">1. Set your redirect URL</div>

View File

@@ -1,12 +1,11 @@
<html>
<html lang="en">
<%- include('partials/head', {title: 'CM OAuth Helper'}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white">
<div class="min-w-screen min-h-screen font-sans">
<%- include('partials/title', {title: ' OAuth Helper'}) %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-600">
<div class="p-6 md:px-10 md:py-6">
<div class="text-xl mb-4">Hi! Looks like you're accepting an invite to authorize an account to run on this ContextMod instance:</div>
<div class="text-lg text-semibold my-3">1. Review permissions</div>

View File

@@ -1,12 +1,11 @@
<html>
<html lang="en">
<%- include('partials/head', {title: 'Access Denied'}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/title', {title: ''}) %>
<div class="container mx-auto">
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-600">
<div class="p-6 md:px-10 md:py-6">
<div class="text-xl mb-4">Sorry!</div>
<div class="space-y-3">
@@ -28,6 +27,5 @@
</div>
<%- include('partials/footer') %>
</div>
<%- include('partials/themeJs') %>
</body>
</html>

View File

@@ -1,14 +1,13 @@
<html>
<html lang="en">
<%- include('partials/head', {title: undefined}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white font-sans">
<div class="min-w-screen min-h-screen">
<%- include('partials/header') %>
<%- include('partials/botsTab') %>
<div class="container mx-auto">
<%- include('partials/subredditsTab') %>
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="bg-gray-700">
<div class="pb-6 md:px-7">
<div class="sub active" data-subreddit="All" data-bot="All">
Instance is currently <b>OFFLINE</b>
@@ -31,7 +30,6 @@
</div>
<%- include('partials/footer') %>
<%- include('partials/instanceTabJs') %>
<%- include('partials/themeJs') %>
<%- include('partials/logSettingsJs') %>
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
<script>

View File

@@ -1,9 +1,9 @@
<div class="space-x-4 py-1 md:px-10 leading-6 font-semibold bg-gray-500 dark:bg-gray-700 text-white">
<div class="space-x-4 py-1 md:px-10 leading-6 font-semibold bg-gray-700">
<div class="container mx-auto">
<% if(locals.bots !== undefined) { %>
<ul id="botTabs" class="inline-flex flex-wrap">
<% bots.forEach(function (data){ %>
<li class="my-3 px-3 dark:text-white">
<li class="my-3 px-3">
<span data-bot="<%= data.system.name %>" class="rounded-md py-2 px-3 tabSelectWrapper">
<a class="tabSelect font-normal pointer hover:font-bold"
data-bot="<%= data.system.name %>">
@@ -16,7 +16,7 @@
</li>
<% }) %>
<% if(locals.isOperator === true && locals.instanceId !== undefined) { %>
<li class="my-3 px-3 dark:text-white">
<li class="my-3 px-3">
<span class="rounded-md py-2 px-3 border">
<a class="font-normal pointer hover:font-bold" href="/auth/helper">
Add Bot +

View File

@@ -1,4 +1,4 @@
<div class="py-3 flex items-center justify-around font-semibold text-white">
<div class="py-3 flex items-center justify-around font-semibold">
<div>
<a href="https://github.com/FoxxMD/context-mod">ContextMod Web</a> created by /u/FoxxMD
</div>

View File

@@ -1,10 +1,5 @@
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind.min.css"
integrity="sha512-wl80ucxCRpLkfaCnbM88y4AxnutbGk327762eM9E/rRTvY/ZGAHWMZrYUq66VQBYMIYDFpDdJAOGSLyIPHZ2IQ=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.3/tailwind-dark.min.css"
integrity="sha512-WvyKyiVHgInX5UQt67447ExtRRZG/8GUijaq1MpqTNYp8wY4/EJOG5bI80sRp/5crDy4Z6bBUydZI2OFV3Vbtg=="
crossorigin="anonymous"/>
<link rel="stylesheet" href="/public/tailwind.min.css"/>
<script src="https://code.iconify.design/1/1.0.4/iconify.min.js"></script>
<link rel="stylesheet" href="/public/themeToggle.css">
<link rel="stylesheet" href="/public/app.css">

View File

@@ -1,11 +1,11 @@
<div class="space-x-4 p-6 md:px-10 md:py-6 leading-6 font-semibold bg-gray-800 text-white">
<div class="space-x-4 p-6 md:px-10 md:py-6 leading-6 font-semibold bg-gray-500 text-white">
<div class="container mx-auto">
<div class="flex items-center justify-between">
<div class="flex items-center flex-grow pr-4">
<% if(locals.instances !== undefined) { %>
<ul class="inline-flex flex-wrap">
<% instances.forEach(function (data) { %>
<li class="my-3 px-3 dark:text-white">
<li class="my-3 px-3">
<span data-instance="<%= data.friendly %>" class="has-tooltip instanceSelectWrapper rounded-md py-3 px-3">
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black' style="margin-top:3em">
<div class="stats">
@@ -34,20 +34,6 @@
<% } %>
</div>
<div class="flex items-center flex-end text-sm">
<span class="inline-block mr-4">
<label style="font-size:2.5px;">
<input class='toggle-checkbox' type='checkbox' id="themeToggle" checked></input>
<div class='toggle-slot'>
<div class='sun-icon-wrapper'>
<div class="iconify sun-icon" data-icon="feather-sun" data-inline="false"></div>
</div>
<div class='toggle-button'></div>
<div class='moon-icon-wrapper'>
<div class="iconify moon-icon" data-icon="feather-moon" data-inline="false"></div>
</div>
</div>
</label>
</span>
<a href="logout">Logout</a>
</div>
</div>

View File

@@ -1,10 +1,10 @@
<div class="space-x-4 pt-2 md:px-5 leading-6 font-semibold bg-gray-white dark:bg-gray-500 text-white">
<div class="space-x-4 pt-2 md:px-5 leading-6 font-semibold bg-gray-800">
<div class="container mx-auto">
<% if(locals.bots !== undefined) { %>
<% bots.forEach(function (botData){ %>
<ul data-bot="<%= botData.system.name %>" class="inline-flex flex-wrap subreddit nestedTabs">
<% botData.subreddits.forEach(function (data){ %>
<li class="my-3 pr-3 dark:text-white">
<li class="my-3 pr-3">
<span data-subreddit="<%= data.name %>" class="rounded-md py-2 px-3 tabSelectWrapper">
<a class="tabSelect font-normal pointer hover:font-bold"
data-subreddit="<%= data.name %>">

View File

@@ -1,32 +0,0 @@
<script>
document.querySelectorAll('.theme').forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
if (e.target.id === 'dark') {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
document.querySelectorAll('.theme').forEach(el => {
el.classList.remove('font-bold', 'no-underline', 'pointer-events-none');
});
e.target.classList.add('font-bold', 'no-underline', 'pointer-events-none');
})
});
const themeToggle = document.querySelector("#themeToggle");
if(themeToggle !== null) {
themeToggle.checked = localStorage.getItem('ms-dark') !== 'no';
themeToggle.onchange = (e) => {
if (e.target.checked === true) {
document.body.classList.add('dark');
localStorage.setItem('ms-dark', 'yes');
} else {
document.body.classList.remove('dark');
localStorage.setItem('ms-dark', 'no');
}
}
}
</script>

View File

@@ -1,4 +1,4 @@
<div class="space-x-4 p-6 md:px-10 md:py-6 leading-6 font-semibold bg-gray-800 text-white">
<div class="space-x-4 p-6 md:px-10 md:py-6 leading-6 font-semibold bg-gray-500">
<div class="container mx-auto">
<div class="flex items-center justify-between">
<div class="flex items-center flex-grow pr-4">
@@ -7,20 +7,7 @@
<% } %>
</div>
<div class="flex items-center flex-end text-sm">
<span class="inline-block mr-4">
<label style="font-size:2.5px;">
<input class='toggle-checkbox' type='checkbox' id="themeToggle" checked></input>
<div class='toggle-slot'>
<div class='sun-icon-wrapper'>
<div class="iconify sun-icon" data-icon="feather-sun" data-inline="false"></div>
</div>
<div class='toggle-button'></div>
<div class='moon-icon-wrapper'>
<div class="iconify moon-icon" data-icon="feather-moon" data-inline="false"></div>
</div>
</div>
</label>
</span>
<a href="logout">Logout</a>
</div>
</div>

View File

@@ -1,21 +1,20 @@
<html>
<html lang="en">
<%- include('partials/head', {title: undefined}) %>
<body class="">
<script>localStorage.getItem('ms-dark') === 'no' ? document.body.classList.remove('dark') : document.body.classList.add('dark')</script>
<div class="min-w-screen min-h-screen bg-gray-100 bg-gray-100 dark:bg-gray-800 font-sans">
<body class="bg-gray-900 text-white">
<div class="min-w-screen min-h-screen font-sans">
<%- include('partials/header') %>
<%- include('partials/botsTab') %>
<%- include('partials/subredditsTab') %>
<div class="container mx-auto">
<%- include('partials/subredditsTab') %>
<div class="grid">
<div class="bg-white dark:bg-gray-500 dark:text-white">
<div class="">
<div class="pb-6 md:px-7">
<% bots.forEach(function (bot){ %>
<% bot.subreddits.forEach(function (data){ %>
<div class="sub <%= bot.system.running ? '' : 'offline' %>" data-subreddit="<%= data.name %>" data-bot="<%= bot.system.name %>">
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3 gap-5">
<div class="bg-white shadow-md rounded my-3 dark:bg-gray-500 dark:text-white">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 dark:bg-gray-700 dark:text-white">
<div class="bg-white shadow-md rounded my-3 bg-gray-600 ">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 bg-gray-700 ">
<div class="flex items-center justify-between">
<h4>Overview</h4>
<% if (data.name === 'All') { %>
@@ -63,10 +62,7 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
@@ -99,10 +95,7 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
@@ -136,10 +129,7 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
@@ -209,11 +199,8 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
</label>
@@ -235,8 +222,8 @@
</div>
</div>
<% if (data.name === 'All') { %>
<div class="bg-white shadow-md rounded my-3 dark:bg-gray-500 dark:text-white">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 dark:bg-gray-700 dark:text-white">
<div class="bg-white shadow-md rounded my-3 bg-gray-600 ">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 bg-gray-700 ">
<h4>API</h4>
</div>
<div class="p-4">
@@ -250,9 +237,40 @@
<label>Api Usage</label>
<span><span id="apiLimit"><%= data.apiLimit %></span>/600 (~<span
id="apiAvg"><%= data.apiAvg %></span>req/s)</span>
<label>Depleted</label>
<label>
<span class="has-tooltip">
<span style="margin-top:55px" class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black space-y-3 p-2 text-left'>
<p>Reddit restricts api usage, per account, to <b>600 requests every 10 minutes.</b></p>
<p><b>Depleted</b> is the estimated time until all 600 requests are used based on <b>Api Usage.</b></p>
<p>API usage is sustainable when <b>Depleted</b> is greater than <b>Limit Reset</b></p>
</span>
<span>
Depleted <svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
</label>
<span>in ~<span id="apiDepletion"><%= data.apiDepletion %></span></span>
<label>Limit Reset</label>
<label>
<span class="has-tooltip">
<span style="margin-top:35px" class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black space-y-3 p-2 text-left'>
<p>Reddit restricts api usage, per account, to <b>600 requests every 10 minutes.</b></p>
<p><b>Limit Reset</b> is the amount of time remaining in the current 10 minute period until the limit is reset to 600.</p>
</span>
<span>
Limit Reset <svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
</label>
<span class="has-tooltip">
<span class='tooltip rounded shadow-lg p-1 bg-gray-100 text-black -mt-2'>
<span id="limitReset"><%= data.limitReset %></span>
@@ -264,8 +282,8 @@
</div>
<% } %>
<% if (data.name !== 'All') { %>
<div class="bg-white shadow-md rounded my-3 dark:bg-gray-500 dark:text-white">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 dark:bg-gray-700 dark:text-white">
<div class="bg-white shadow-md rounded my-3 bg-gray-600 ">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-700 ">
<h4>Config
<span>
<span class="has-tooltip">
@@ -316,8 +334,8 @@
</div>
</div>
<% } %>
<div class="bg-white shadow-md rounded my-3 dark:bg-gray-500 dark:text-white">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-300 dark:bg-gray-700 dark:text-white">
<div class="bg-white shadow-md rounded my-3 bg-gray-600 ">
<div class="space-x-4 px-4 p-2 leading-2 font-semibold bg-gray-700 ">
<h4>Usage</h4>
</div>
<div class="p-4">
@@ -440,10 +458,7 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
@@ -580,10 +595,7 @@
class="h-4 w-4 inline-block cursor-help"
fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<use xlink:href="public/questionsymbol.svg#q" />
</svg>
</span>
</span>
@@ -628,7 +640,6 @@
<%- include('partials/footer') %>
</div>
<%- include('partials/instanceTabJs') %>
<%- include('partials/themeJs') %>
<%- include('partials/logSettingsJs') %>
<script>
window.sort = 'desc';

View File

@@ -756,11 +756,11 @@ export const formatLogLineToHtml = (log: string | LogInfo) => {
stripPrefix: false,
sanitizeHtml: true,
})
.replace(/(\s*debug\s*):/i, '<span class="debug text-pink-400">$1</span>:')
.replace(/(\s*warn\s*):/i, '<span class="warn text-yellow-400">$1</span>:')
.replace(/(\s*info\s*):/i, '<span class="info text-blue-300">$1</span>:')
.replace(/(\s*error\s*):/i, '<span class="error text-red-400">$1</span>:')
.replace(/(\s*verbose\s*):/i, '<span class="error text-purple-400">$1</span>:')
.replace(/(\s*debug\s*):/i, '<span class="debug blue">$1</span>:')
.replace(/(\s*warn\s*):/i, '<span class="warn yellow">$1</span>:')
.replace(/(\s*info\s*):/i, '<span class="info green">$1</span>:')
.replace(/(\s*error\s*):/i, '<span class="error red">$1</span>:')
.replace(/(\s*verbose\s*):/i, '<span class="error purple">$1</span>:')
.replaceAll('\n', '<br />');
//.replace(HYPERLINK_REGEX, '<a target="_blank" href="$&">$&</a>');
return `<div class="logLine">${logContent}</div>`
@@ -969,6 +969,13 @@ export const toStrongSubredditState = (s: SubredditState, opts?: StrongSubreddit
return strongState;
}
export const convertSubredditsRawToStrong = (x: (SubredditState | string), opts: StrongSubredditStateOptions): StrongSubredditState => {
if (typeof x === 'string') {
return toStrongSubredditState({name: x, stateDescription: x}, opts);
}
return toStrongSubredditState(x, opts);
}
export async function readConfigFile(path: string, opts: any) {
const {log, throwOnNotFound = true} = opts;
try {
@@ -1413,3 +1420,7 @@ export const absPercentDifference = (num1: number, num2: number) => {
export const bitsToHexLength = (bits: number): number => {
return Math.pow(bits, 2) / 4;
}
export const escapeRegex = (val: string) => {
return val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}