Compare commits

..

8 Commits

Author SHA1 Message Date
FoxxMD
1956d04e79 fix(delayed): Prevent delete call when no ids found 2022-11-15 14:36:42 -05:00
FoxxMD
d5bba8ca87 fix(delayed): Fix missing submission accessor 2022-11-15 14:22:08 -05:00
FoxxMD
834fca11d5 Merge branch 'edge' into dispatchedActionActivity 2022-11-15 14:17:56 -05:00
FoxxMD
54917a562e fix(delayed): Fix accessing non existent actioned events 2022-11-15 14:14:53 -05:00
FoxxMD
122391f5f7 Merge branch 'edge' into dispatchedActionActivity 2022-11-15 09:37:03 -05:00
FoxxMD
0542b6debb Merge branch 'edge' into dispatchedActionActivity 2022-11-15 09:06:11 -05:00
FoxxMD
e05f350b37 feat: Implement orphaned activity cleanup on delayed activity deletion
Make sure we delete Activities that were inserted on dispatched actions BUT ONLY if they are not used anywhere else (events or other delayed activities)
2022-11-03 13:42:01 -04:00
FoxxMD
a23b5d6b06 feat: Refactor Dispatched Action db entity to use full-fat Activity
* Instead of storing limited info about an Activity in the table just persist the full Activity with a relationship
* Fixes issue on CM init where snoowrap needs to fetch all activities for dispatched actions in order to get permalinks and simplifies things in general
2022-11-03 10:01:14 -04:00
32 changed files with 607 additions and 3522 deletions

View File

@@ -24,9 +24,6 @@ services:
cache:
image: 'redis:7-alpine'
volumes:
# on linux will need to make sure this directory has correct permissions for container to access
- './data/cache:/data'
database:
image: 'mariadb:10.9.3'

View File

@@ -55,8 +55,6 @@ The included [`docker-compose.yml`](/docker-compose.yml) provides production-rea
#### Setup
The included `docker-compose.yml` file is written for **Docker Compose v2.**
For new installations copy [`config.yaml`](/docker/config/docker-compose/config.yaml) into a folder named `data` in the same folder `docker-compose.yml` will be run from. For users migrating their existing CM instances to docker-compose, copy your existing `config.yaml` into the same `data` folder.
Read through the comments in both `docker-compose.yml` and `config.yaml` and makes changes to any relevant settings (passwords, usernames, etc...). Ensure that any settings used in both files (EX mariaDB passwords) match.
@@ -64,13 +62,13 @@ Read through the comments in both `docker-compose.yml` and `config.yaml` and mak
To build and start CM:
```bash
docker compose up -d
docker-compose up -d
```
To include Grafana/Influx dependencies run:
```bash
docker compose --profile full up -d
docker-compose --profile full up -d
```
## Locally

View File

@@ -57,42 +57,17 @@ All Actions with `content` have access to this data:
| `title` | As comments => the body of the comment. As Submission => title | Test post please ignore |
| `shortTitle` | The same as `title` but truncated to 15 characters | test post pleas... |
#### Common Author
Additionally, `author` has these properties accessible:
| Name | Description | Example |
|----------------|-----------------------------------------------------------------------------------|------------|
| `age` | (Approximate) Age of account | 3 months |
| `linkKarma` | Amount of link karma | 10 |
| `commentKarma` | Amount of comment karma | 3 |
| `totalKarma` | Combined link+comment karma | 13 |
| `verified` | Does account have a verified email? | true |
| `flairText` | The text of the Flair assigned to the Author in this subreddit, if one is present | Test Flair |
NOTE: Accessing these properties may require an additional API call so use sparingly on high-volume comments
##### Example Usage
```
The user {{item.author}} has been a redditor for {{item.author.age}}
```
Produces:
> The user FoxxMD has been a redditor for 3 months
### Submissions
If the **Activity** is a Submission these additional properties are accessible:
| Name | Description | Example |
|-------------------|-----------------------------------------------------------------|-------------------------|
| `upvoteRatio` | The upvote ratio | 100% |
| `nsfw` | If the submission is marked as NSFW | true |
| `spoiler` | If the submission is marked as a spoiler | true |
| `url` | If the submission was a link then this is the URL for that link | http://example.com |
| `title` | The title of the submission | Test post please ignore |
| `link_flair_text` | The flair text assigned to this submission | Test Flair |
| Name | Description | Example |
|---------------|-----------------------------------------------------------------|-------------------------|
| `upvoteRatio` | The upvote ratio | 100% |
| `nsfw` | If the submission is marked as NSFW | true |
| `spoiler` | If the submission is marked as a spoiler | true |
| `url` | If the submission was a link then this is the URL for that link | http://example.com |
| `title` | The title of the submission | Test post please ignore |
### Comments

View File

@@ -67,15 +67,13 @@ export class BanAction extends Action {
// @ts-ignore
const fetchedSub = await item.subreddit.fetch();
const fetchedName = await item.author.name;
const banData = {
const bannedUser = await fetchedSub.banUser({
name: fetchedName,
banMessage: renderedContent === undefined ? undefined : renderedContent,
banReason: renderedReason,
banNote: renderedNote,
duration: this.duration
};
const bannedUser = await fetchedSub.banUser(banData);
await this.resources.addUserToSubredditBannedUserCache(banData)
});
touchedEntities.push(bannedUser);
}
return {

View File

@@ -30,25 +30,30 @@ export class FlairAction extends Action {
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
const dryRun = this.getRuntimeAwareDryrun(options);
let flairParts = [];
const renderedText = this.text === '' ? '' : await this.renderContent(this.text, item, ruleResults, actionResults) as string;
flairParts.push(`Text: ${renderedText === '' ? '(None)' : renderedText}`);
const renderedCss = this.css === '' ? '' : await this.renderContent(this.css, item, ruleResults, actionResults) as string;
flairParts.push(`CSS: ${renderedCss === '' ? '(None)' : renderedCss}`);
flairParts.push(`Template: ${this.flair_template_id === '' ? '(None)' : this.flair_template_id}`);
if(this.text !== '') {
flairParts.push(`Text: ${this.text}`);
}
if(this.css !== '') {
flairParts.push(`CSS: ${this.css}`);
}
if(this.flair_template_id !== '') {
flairParts.push(`Template: ${this.flair_template_id}`);
}
const flairSummary = flairParts.length === 0 ? 'No flair (unflaired)' : flairParts.join(' | ');
this.logger.verbose(flairSummary);
if (item instanceof Submission) {
if(!this.dryRun) {
if (this.flair_template_id) {
await item.selectFlair({flair_template_id: this.flair_template_id}).then(() => {});
// typings are wrong for this function, flair_template_id should be accepted
// assignFlair uses /api/flair (mod endpoint)
// selectFlair uses /api/selectflair (self endpoint for user to choose their own flair for submission)
// @ts-ignore
await item.assignFlair({flair_template_id: this.flair_template_id}).then(() => {});
item.link_flair_template_id = this.flair_template_id;
} else {
await item.assignFlair({text: renderedText, cssClass: renderedCss}).then(() => {});
item.link_flair_css_class = renderedCss;
item.link_flair_text = renderedText;
await item.assignFlair({text: this.text, cssClass: this.css}).then(() => {});
item.link_flair_css_class = this.css;
item.link_flair_text = this.text;
}
await this.resources.resetCacheForItem(item);
}

View File

@@ -26,8 +26,6 @@ export class UserFlairAction extends Action {
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
const dryRun = this.getRuntimeAwareDryrun(options);
let flairParts = [];
let renderedText: string | undefined = undefined;
let renderedCss: string | undefined = undefined;
if (this.flair_template_id !== undefined) {
flairParts.push(`Flair template ID: ${this.flair_template_id}`)
@@ -36,12 +34,10 @@ export class UserFlairAction extends Action {
}
} else {
if (this.text !== undefined) {
renderedText = await this.renderContent(this.text, item, ruleResults, actionResults) as string;
flairParts.push(`Text: ${renderedText}`);
flairParts.push(`Text: ${this.text}`);
}
if (this.css !== undefined) {
renderedCss = await this.renderContent(this.css, item, ruleResults, actionResults) as string;
flairParts.push(`CSS: ${renderedCss}`);
flairParts.push(`CSS: ${this.css}`);
}
}
@@ -62,7 +58,7 @@ export class UserFlairAction extends Action {
this.logger.error('Either the flair template ID is incorrect or you do not have permission to access it.');
throw err;
}
} else if (renderedText === undefined && renderedCss === undefined) {
} else if (this.text === undefined && this.css === undefined) {
// @ts-ignore
await item.subreddit.deleteUserFlair(item.author.name);
item.author_flair_css_class = null;
@@ -72,11 +68,11 @@ export class UserFlairAction extends Action {
// @ts-ignore
await item.author.assignFlair({
subredditName: item.subreddit.display_name,
cssClass: renderedCss,
text: renderedText,
cssClass: this.css,
text: this.text,
});
item.author_flair_text = renderedText ?? null;
item.author_flair_css_class = renderedCss ?? null;
item.author_flair_text = this.text ?? null;
item.author_flair_css_class = this.css ?? null;
}
await this.resources.resetCacheForItem(item);
if(typeof item.author !== 'string') {

View File

@@ -46,13 +46,7 @@ import {RunStateType} from "../Common/Entities/RunStateType";
import {QueueRunState} from "../Common/Entities/EntityRunState/QueueRunState";
import {EventsRunState} from "../Common/Entities/EntityRunState/EventsRunState";
import {ManagerRunState} from "../Common/Entities/EntityRunState/ManagerRunState";
import {
Invokee,
POLLING_COMMENTS, POLLING_MODQUEUE,
POLLING_SUBMISSIONS,
POLLING_UNMODERATED,
PollOn
} from "../Common/Infrastructure/Atomic";
import {Invokee, PollOn} from "../Common/Infrastructure/Atomic";
import {FilterCriteriaDefaults} from "../Common/Infrastructure/Filters/FilterShapes";
import {snooLogWrapper} from "../Utils/loggerFactory";
import {InfluxClient} from "../Common/Influx/InfluxClient";
@@ -564,9 +558,9 @@ class Bot implements BotInstanceFunctions {
parseSharedStreams() {
const sharedCommentsSubreddits = !this.sharedStreams.includes(POLLING_COMMENTS) ? [] : this.subManagers.filter(x => x.isPollingShared(POLLING_COMMENTS)).map(x => x.subreddit.display_name);
const sharedCommentsSubreddits = !this.sharedStreams.includes('newComm') ? [] : this.subManagers.filter(x => x.isPollingShared('newComm')).map(x => x.subreddit.display_name);
if (sharedCommentsSubreddits.length > 0) {
const stream = this.cacheManager.modStreams.get(POLLING_COMMENTS);
const stream = this.cacheManager.modStreams.get('newComm');
if (stream === undefined || stream.subreddit !== sharedCommentsSubreddits.join('+')) {
let processed;
if (stream !== undefined) {
@@ -586,20 +580,20 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultCommentStream.on('error', this.createSharedStreamErrorListener(POLLING_COMMENTS));
defaultCommentStream.on('listing', this.createSharedStreamListingListener(POLLING_COMMENTS));
this.cacheManager.modStreams.set(POLLING_COMMENTS, defaultCommentStream);
defaultCommentStream.on('error', this.createSharedStreamErrorListener('newComm'));
defaultCommentStream.on('listing', this.createSharedStreamListingListener('newComm'));
this.cacheManager.modStreams.set('newComm', defaultCommentStream);
}
} else {
const stream = this.cacheManager.modStreams.get(POLLING_COMMENTS);
const stream = this.cacheManager.modStreams.get('newComm');
if (stream !== undefined) {
stream.end('Determined no managers are listening on shared stream parsing');
}
}
const sharedSubmissionsSubreddits = !this.sharedStreams.includes(POLLING_SUBMISSIONS) ? [] : this.subManagers.filter(x => x.isPollingShared(POLLING_SUBMISSIONS)).map(x => x.subreddit.display_name);
const sharedSubmissionsSubreddits = !this.sharedStreams.includes('newSub') ? [] : this.subManagers.filter(x => x.isPollingShared('newSub')).map(x => x.subreddit.display_name);
if (sharedSubmissionsSubreddits.length > 0) {
const stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS);
const stream = this.cacheManager.modStreams.get('newSub');
if (stream === undefined || stream.subreddit !== sharedSubmissionsSubreddits.join('+')) {
let processed;
if (stream !== undefined) {
@@ -619,19 +613,19 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultSubStream.on('error', this.createSharedStreamErrorListener(POLLING_SUBMISSIONS));
defaultSubStream.on('listing', this.createSharedStreamListingListener(POLLING_SUBMISSIONS));
this.cacheManager.modStreams.set(POLLING_SUBMISSIONS, defaultSubStream);
defaultSubStream.on('error', this.createSharedStreamErrorListener('newSub'));
defaultSubStream.on('listing', this.createSharedStreamListingListener('newSub'));
this.cacheManager.modStreams.set('newSub', defaultSubStream);
}
} else {
const stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS);
const stream = this.cacheManager.modStreams.get('newSub');
if (stream !== undefined) {
stream.end('Determined no managers are listening on shared stream parsing');
}
}
const isUnmoderatedShared = !this.sharedStreams.includes(POLLING_UNMODERATED) ? false : this.subManagers.some(x => x.isPollingShared(POLLING_UNMODERATED));
const unmoderatedstream = this.cacheManager.modStreams.get(POLLING_UNMODERATED);
const isUnmoderatedShared = !this.sharedStreams.includes('unmoderated') ? false : this.subManagers.some(x => x.isPollingShared('unmoderated'));
const unmoderatedstream = this.cacheManager.modStreams.get('unmoderated');
if (isUnmoderatedShared && unmoderatedstream === undefined) {
const defaultUnmoderatedStream = new UnmoderatedStream(this.client, {
subreddit: 'mod',
@@ -640,15 +634,15 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultUnmoderatedStream.on('error', this.createSharedStreamErrorListener(POLLING_UNMODERATED));
defaultUnmoderatedStream.on('listing', this.createSharedStreamListingListener(POLLING_UNMODERATED));
this.cacheManager.modStreams.set(POLLING_UNMODERATED, defaultUnmoderatedStream);
defaultUnmoderatedStream.on('error', this.createSharedStreamErrorListener('unmoderated'));
defaultUnmoderatedStream.on('listing', this.createSharedStreamListingListener('unmoderated'));
this.cacheManager.modStreams.set('unmoderated', defaultUnmoderatedStream);
} else if (!isUnmoderatedShared && unmoderatedstream !== undefined) {
unmoderatedstream.end('Determined no managers are listening on shared stream parsing');
}
const isModqueueShared = !this.sharedStreams.includes(POLLING_MODQUEUE) ? false : this.subManagers.some(x => x.isPollingShared(POLLING_MODQUEUE));
const modqueuestream = this.cacheManager.modStreams.get(POLLING_MODQUEUE);
const isModqueueShared = !this.sharedStreams.includes('modqueue') ? false : this.subManagers.some(x => x.isPollingShared('modqueue'));
const modqueuestream = this.cacheManager.modStreams.get('modqueue');
if (isModqueueShared && modqueuestream === undefined) {
const defaultModqueueStream = new ModQueueStream(this.client, {
subreddit: 'mod',
@@ -657,9 +651,9 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultModqueueStream.on('error', this.createSharedStreamErrorListener(POLLING_MODQUEUE));
defaultModqueueStream.on('listing', this.createSharedStreamListingListener(POLLING_MODQUEUE));
this.cacheManager.modStreams.set(POLLING_MODQUEUE, defaultModqueueStream);
defaultModqueueStream.on('error', this.createSharedStreamErrorListener('modqueue'));
defaultModqueueStream.on('listing', this.createSharedStreamListingListener('modqueue'));
this.cacheManager.modStreams.set('modqueue', defaultModqueueStream);
} else if (isModqueueShared && modqueuestream !== undefined) {
modqueuestream.end('Determined no managers are listening on shared stream parsing');
}

View File

@@ -1,4 +1,4 @@
import {Entity, Column, ManyToOne, PrimaryColumn, OneToMany, Index} from "typeorm";
import {Entity, Column, ManyToOne, PrimaryColumn, OneToMany, Index, DataSource, JoinColumn} from "typeorm";
import {AuthorEntity} from "./AuthorEntity";
import {Subreddit} from "./Subreddit";
import {CMEvent} from "./CMEvent";
@@ -6,6 +6,8 @@ import {asComment, getActivityAuthorName, parseRedditFullname, redditThingTypeTo
import {activityReports, ActivityType, Report, SnoowrapActivity} from "../Infrastructure/Reddit";
import {ActivityReport} from "./ActivityReport";
import dayjs, {Dayjs} from "dayjs";
import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients";
import {Comment, Submission} from 'snoowrap/dist/objects';
export interface ActivityEntityOptions {
id: string
@@ -45,7 +47,7 @@ export class Activity {
@Column({name: 'name'})
name!: string;
@ManyToOne(type => Subreddit, sub => sub.activities, {cascade: ['insert']})
@ManyToOne(type => Subreddit, sub => sub.activities, {cascade: ['insert'], eager: true})
subreddit!: Subreddit;
@Column("varchar", {length: 20})
@@ -58,17 +60,18 @@ export class Activity {
@Column("text")
permalink!: string;
@ManyToOne(type => AuthorEntity, author => author.activities, {cascade: ['insert']})
@ManyToOne(type => AuthorEntity, author => author.activities, {cascade: ['insert'], eager: true})
author!: AuthorEntity;
@OneToMany(type => CMEvent, act => act.activity) // note: we will create author property in the Photo class below
@OneToMany(type => CMEvent, act => act.activity)
actionedEvents!: CMEvent[]
@ManyToOne(type => Activity, obj => obj.comments, {nullable: true})
@ManyToOne('Activity', 'comments', {nullable: true, cascade: ['insert']})
@JoinColumn({name: 'submission_id'})
submission?: Activity;
@OneToMany(type => Activity, obj => obj.submission, {nullable: true})
comments!: Activity[];
@OneToMany('Activity', 'submission', {nullable: true})
comments?: Activity[];
@OneToMany(type => ActivityReport, act => act.activity, {cascade: ['insert'], eager: true})
reports: ActivityReport[] | undefined
@@ -151,10 +154,12 @@ export class Activity {
return false;
}
static fromSnoowrapActivity(subreddit: Subreddit, activity: SnoowrapActivity, lastKnownStateTimestamp?: dayjs.Dayjs | undefined) {
static async fromSnoowrapActivity(activity: SnoowrapActivity, options: fromSnoowrapOptions | undefined = {}) {
let submission: Activity | undefined;
let type: ActivityType = 'submission';
let content: string;
const subreddit = await Subreddit.fromSnoowrap(activity.subreddit, options?.db);
if(asComment(activity)) {
type = 'comment';
content = activity.body;
@@ -179,8 +184,30 @@ export class Activity {
submission
});
entity.syncReports(activity, lastKnownStateTimestamp);
entity.syncReports(activity, options.lastKnownStateTimestamp);
return entity;
}
toSnoowrap(client: ExtendedSnoowrap): SnoowrapActivity {
let act: SnoowrapActivity;
if(this.type === 'submission') {
act = new Submission({name: this.id, id: this.name}, client, false);
act.title = this.content;
} else {
act = new Comment({name: this.id, id: this.name}, client, false);
act.link_id = this.submission?.id as string;
act.body = this.content;
}
act.permalink = this.permalink;
act.subreddit = this.subreddit.toSnoowrap(client);
act.author = this.author.toSnoowrap(client);
return act;
}
}
export interface fromSnoowrapOptions {
lastKnownStateTimestamp?: dayjs.Dayjs | undefined
db?: DataSource
}

View File

@@ -1,5 +1,8 @@
import {Entity, Column, PrimaryColumn, OneToMany} from "typeorm";
import {Activity} from "./Activity";
import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients";
import {SnoowrapActivity} from "../Infrastructure/Reddit";
import {RedditUser} from "snoowrap/dist/objects";
@Entity({name: 'Author'})
export class AuthorEntity {
@@ -11,11 +14,15 @@ export class AuthorEntity {
name!: string;
@OneToMany(type => Activity, act => act.author)
activities!: Activity[]
activities!: Promise<Activity[]>
constructor(data?: any) {
if(data !== undefined) {
this.name = data.name;
}
}
toSnoowrap(client: ExtendedSnoowrap): RedditUser {
return new RedditUser({name: this.name, id: this.id}, client, false);
}
}

View File

@@ -6,7 +6,7 @@ import {
ManyToOne,
PrimaryColumn,
BeforeInsert,
AfterLoad
AfterLoad, JoinColumn
} from "typeorm";
import {
ActivityDispatch
@@ -22,15 +22,15 @@ import Comment from "snoowrap/dist/objects/Comment";
import {ColumnDurationTransformer} from "./Transformers";
import { RedditUser } from "snoowrap/dist/objects";
import {ActivitySourceTypes, DurationVal, NonDispatchActivitySourceValue, onExistingFoundBehavior} from "../Infrastructure/Atomic";
import {Activity} from "./Activity";
@Entity({name: 'DispatchedAction'})
export class DispatchedEntity extends TimeAwareRandomBaseEntity {
@Column()
activityId!: string
@Column()
author!: string
//@ManyToOne(type => Activity, obj => obj.dispatched, {cascade: ['insert'], eager: true, nullable: false})
@ManyToOne(type => Activity, undefined, {cascade: ['insert'], eager: true, nullable: false})
@JoinColumn({name: 'activityId'})
activity!: Activity
@Column({
type: 'int',
@@ -82,11 +82,10 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
}})
tardyTolerant!: boolean | Duration
constructor(data?: ActivityDispatch & { manager: ManagerEntity }) {
constructor(data?: HydratedActivityDispatch) {
super();
if (data !== undefined) {
this.activityId = data.activity.name;
this.author = getActivityAuthorName(data.activity.author);
this.activity = data.activity;
this.delay = data.delay;
this.createdAt = data.queuedAt;
this.type = data.type;
@@ -151,20 +150,7 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
}
async toActivityDispatch(client: ExtendedSnoowrap): Promise<ActivityDispatch> {
const redditThing = parseRedditFullname(this.activityId);
if(redditThing === undefined) {
throw new Error(`Could not parse reddit ID from value '${this.activityId}'`);
}
let activity: Comment | Submission;
if (redditThing?.type === 'comment') {
// @ts-ignore
activity = await client.getComment(redditThing.id);
} else {
// @ts-ignore
activity = await client.getSubmission(redditThing.id);
}
activity.author = new RedditUser({name: this.author}, client, false);
activity.id = redditThing.id;
let activity = this.activity.toSnoowrap(client);
return {
id: this.id,
queuedAt: this.createdAt,
@@ -176,8 +162,13 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
cancelIfQueued: this.cancelIfQueued,
identifier: this.identifier,
type: this.type,
author: this.author,
author: activity.author.name,
dryRun: this.dryRun
}
}
}
export interface HydratedActivityDispatch extends Omit<ActivityDispatch, 'activity'> {
activity: Activity
manager: ManagerEntity
}

View File

@@ -1,5 +1,7 @@
import {Entity, Column, PrimaryColumn, OneToMany, Index} from "typeorm";
import {Entity, Column, PrimaryColumn, OneToMany, Index, DataSource} from "typeorm";
import {Activity} from "./Activity";
import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients";
import {Subreddit as SnoowrapSubreddit} from "snoowrap/dist/objects";
export interface SubredditEntityOptions {
id: string
@@ -25,4 +27,18 @@ export class Subreddit {
this.name = data.name;
}
}
toSnoowrap(client: ExtendedSnoowrap): SnoowrapSubreddit {
return new SnoowrapSubreddit({display_name: this.name, name: this.id}, client, false);
}
static async fromSnoowrap(subreddit: SnoowrapSubreddit, db?: DataSource) {
if(db !== undefined) {
const existing = await db.getRepository(Subreddit).findOneBy({name: subreddit.display_name});
if(existing) {
return existing;
}
}
return new Subreddit({id: await subreddit.name, name: await subreddit.display_name});
}
}

View File

@@ -111,19 +111,6 @@ export interface DurationObject {
export type JoinOperands = 'OR' | 'AND';
export type PollOn = 'unmoderated' | 'modqueue' | 'newSub' | 'newComm';
export const POLLING_UNMODERATED: PollOn = 'unmoderated';
export const POLLING_MODQUEUE: PollOn = 'modqueue';
export const POLLING_SUBMISSIONS: PollOn = 'newSub';
export const POLLING_COMMENTS: PollOn = 'newComm';
export const pollOnTypes: PollOn[] = [POLLING_UNMODERATED, POLLING_MODQUEUE, POLLING_SUBMISSIONS, POLLING_COMMENTS];
export const pollOnTypeMapping: Map<string, PollOn> = new Map([
['unmoderated', POLLING_UNMODERATED],
['modqueue', POLLING_MODQUEUE],
['newsub', POLLING_SUBMISSIONS],
['newcomm', POLLING_COMMENTS],
// be nice if user mispelled
['newcom', POLLING_COMMENTS]
]);
export type ModeratorNames = 'self' | 'automod' | 'automoderator' | string;
export type Invokee = 'system' | 'user';
export type RunState = 'running' | 'paused' | 'stopped';
@@ -408,9 +395,3 @@ export interface RuleResultsTemplateData {
export interface GenericContentTemplateData extends BaseTemplateData, Partial<RuleResultsTemplateData>, Partial<ActionResultsTemplateData> {
item?: (SubmissionTemplateData | CommentTemplateData)
}
export type SubredditPlaceholderType = '{{subreddit}}';
export const subredditPlaceholder: SubredditPlaceholderType = '{{subreddit}}';
export const asSubredditPlaceholder = (val: any): val is SubredditPlaceholderType => {
return typeof val === 'string' && val.toLowerCase() === '{{subreddit}}';
}

View File

@@ -4,7 +4,7 @@ import {
DurationComparor,
ModeratorNameCriteria,
ModeratorNames, ModActionType,
ModUserNoteLabel, RelativeDateTimeMatch, SubredditPlaceholderType
ModUserNoteLabel, RelativeDateTimeMatch
} from "../Atomic";
import {ActivityType, MaybeActivityType} from "../Reddit";
import {GenericComparison, parseGenericValueComparison} from "../Comparisons";
@@ -57,7 +57,7 @@ export interface SubredditCriteria {
}
export interface StrongSubredditCriteria extends SubredditCriteria {
name?: RegExp | SubredditPlaceholderType
name?: RegExp
}
export const defaultStrongSubredditCriteriaOptions = {
@@ -289,57 +289,7 @@ export const toFullModLogCriteria = (val: ModLogCriteria): FullModLogCriteria =>
}, {});
}
export const authorCriteriaProperties = ['name', 'flairCssClass', 'flairText', 'flairTemplate', 'isMod', 'userNotes', 'modActions', 'age', 'linkKarma', 'commentKarma', 'totalKarma', 'verified', 'shadowBanned', 'description', 'isContributor', 'banned'];
export interface BanCriteria {
/**
* Test when the Author was banned against this comparison
*
* The syntax is `(< OR > OR <= OR >=) <number> <unit>`
*
* * EX `> 100 days` => Passes if Author was banned more than 100 days ago
* * EX `<= 2 months` => Passes if Author was banned less than or equal to 2 months ago
*
* Unit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)
*
* [See] https://regexr.com/609n8 for example
*
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\s*$
* */
bannedAt?: DurationComparor
/**
* A string or list of strings to match ban note against.
*
* If a list then ANY matched string makes this pass.
*
* String may be a regular expression enclosed in forward slashes. If it is not a regular expression then it is matched case-insensitive.
* */
note?: string | string[]
/**
* Test how many days are left for Author's ban against this comparison
*
* If the ban is permanent then the number of days left is equivalent to **INFINITY**
*
* The syntax is `(< OR > OR <= OR >=) <number> <unit>`
*
* * EX `> 100 days` => Passes if the Author's ban has more than 100 days left
* * EX `<= 2 months` => Passes if Author's ban has equal to or less than 2 months left
*
* Unit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)
*
* [See] https://regexr.com/609n8 for example
*
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\s*$
* */
daysLeft?: DurationComparor
/**
* Is the ban permanent?
* */
permanent?: boolean
}
export const authorCriteriaProperties = ['name', 'flairCssClass', 'flairText', 'flairTemplate', 'isMod', 'userNotes', 'modActions', 'age', 'linkKarma', 'commentKarma', 'totalKarma', 'verified', 'shadowBanned', 'description', 'isContributor'];
/**
* Criteria with which to test against the author of an Activity. The outcome of the test is based on:
@@ -478,19 +428,6 @@ export interface AuthorCriteria {
* Is the author an approved user (contributor)?
* */
isContributor?: boolean
/**
* Is the Author banned or not?
*
* If user is not banned but BanCriteria(s) is present the test will fail
*
* * Use a boolean true/false for a simple yes or no
* * Or use a BanCriteria to test for specifics of an existing ban
* * Or use a list of BanCriteria -- if ANY BanCriteria passes the test passes
*
* NOTE: USE WITH CARE! This criteria usually incurs 1 API call
* */
banned?: boolean | BanCriteria | BanCriteria[]
}
/**
@@ -518,7 +455,6 @@ export const orderedAuthorCriteriaProps: (keyof AuthorCriteria)[] = [
'isMod', // requires fetching mods for subreddit
'isContributor', // requires fetching contributors for subreddit
'modActions', // requires fetching mod notes/actions for author (shortest cache TTL)
'banned', // requires fetching /about/banned for every user not cached
];
export interface ActivityState {

View File

@@ -1,9 +1,6 @@
import {Comment, RedditUser, Submission, Subreddit} from "snoowrap/dist/objects";
import { BannedUser } from "snoowrap/dist/objects/Subreddit";
import { ValueOf } from "ts-essentials";
import {CMError} from "../../Utils/Errors";
import {Dayjs} from "dayjs";
import {Duration} from "dayjs/plugin/duration.js";
export type ActivityType = 'submission' | 'comment';
export type MaybeActivityType = ActivityType | false;
@@ -169,16 +166,3 @@ export interface RedditRemovalMessageOptions {
title?: string
lock?: boolean
}
export interface CMBannedUser extends Omit<SnoowrapBannedUser, 'days_left' | 'date'> {
user: RedditUser
date: Dayjs
days_left: undefined | Duration
}
export interface SnoowrapBannedUser extends Omit<BannedUser, 'id'> {
days_left: number | null
rel_id?: string
id?: string
}

View File

@@ -0,0 +1,19 @@
import {MigrationInterface, QueryRunner, TableColumn} from "typeorm"
export class delayedReset1667415256831 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
queryRunner.connection.logger.logSchemaBuild('Truncating (removing) existing Dispatched Actions due to internal structural changes');
await queryRunner.clearTable('DispatchedAction');
await queryRunner.changeColumn('DispatchedAction', 'author', new TableColumn({
name: 'author',
type: 'varchar',
length: '150',
isNullable: true
}));
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@@ -372,7 +372,7 @@ export interface PollingOptions extends PollingDefaults {
* * after they have been manually approved from modqueue
*
* */
pollOn: PollOn
pollOn: 'unmoderated' | 'modqueue' | 'newSub' | 'newComm'
}
export interface TTLConfig {

View File

@@ -8,7 +8,7 @@ import {
overwriteMerge,
parseBool, parseExternalUrl, parseUrlContext, parseWikiContext, randomId,
readConfigFile,
removeUndefinedKeys, resolvePathFromEnvWithRelative, toPollOn, toStrongSharingACLConfig
removeUndefinedKeys, resolvePathFromEnvWithRelative, toStrongSharingACLConfig
} from "./util";
import Ajv, {Schema} from 'ajv';
@@ -74,8 +74,8 @@ import {ErrorWithCause} from "pony-cause";
import {RunConfigHydratedData, RunConfigData, RunConfigObject} from "./Run";
import {AuthorRuleConfig} from "./Rule/AuthorRule";
import {
CacheProvider, ConfigFormat, ConfigFragmentParseFunc, POLLING_MODQUEUE, POLLING_UNMODERATED,
PollOn, pollOnTypes
CacheProvider, ConfigFormat, ConfigFragmentParseFunc,
PollOn
} from "./Common/Infrastructure/Atomic";
import {
asFilterOptionsJson,
@@ -452,31 +452,27 @@ export class ConfigBuilder {
export const buildPollingOptions = (values: (string | PollingOptions)[]): PollingOptionsStrong[] => {
let opts: PollingOptionsStrong[] = [];
let rawOpts: PollingOptions;
for (const v of values) {
if (typeof v === 'string') {
rawOpts = {pollOn: v as PollOn}; // maybeee
opts.push({
pollOn: v as PollOn,
interval: DEFAULT_POLLING_INTERVAL,
limit: DEFAULT_POLLING_LIMIT,
});
} else {
rawOpts = v;
const {
pollOn: p,
interval = DEFAULT_POLLING_INTERVAL,
limit = DEFAULT_POLLING_LIMIT,
delayUntil,
} = v;
opts.push({
pollOn: p as PollOn,
interval,
limit,
delayUntil,
});
}
const {
pollOn: p,
interval = DEFAULT_POLLING_INTERVAL,
limit = DEFAULT_POLLING_LIMIT,
delayUntil,
} = rawOpts;
const pVal = toPollOn(p);
if (opts.some(x => x.pollOn === pVal)) {
throw new SimpleError(`Polling source ${pVal} cannot appear more than once in polling options`);
}
opts.push({
pollOn: pVal,
interval,
limit,
delayUntil,
});
}
return opts;
}
@@ -800,7 +796,7 @@ export const parseDefaultBotInstanceFromArgs = (args: any): BotInstanceJsonConfi
heartbeatInterval: heartbeat,
},
polling: {
shared: sharedMod ? [POLLING_UNMODERATED, POLLING_MODQUEUE] : undefined,
shared: sharedMod ? ['unmoderated', 'modqueue'] : undefined,
},
nanny: {
softLimit,
@@ -912,7 +908,7 @@ export const parseDefaultBotInstanceFromEnv = (): BotInstanceJsonConfig => {
heartbeatInterval: process.env.HEARTBEAT !== undefined ? parseInt(process.env.HEARTBEAT) : undefined,
},
polling: {
shared: parseBool(process.env.SHARE_MOD) ? [POLLING_UNMODERATED, POLLING_MODQUEUE] : undefined,
shared: parseBool(process.env.SHARE_MOD) ? ['unmoderated', 'modqueue'] : undefined,
},
nanny: {
softLimit: process.env.SOFT_LIMIT !== undefined ? parseInt(process.env.SOFT_LIMIT) : undefined,
@@ -1529,10 +1525,10 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
botCache.provider.prefix = buildCachePrefix([botCache.provider.prefix, 'bot', (botName || objectHash.sha1(botCreds))]);
}
let realShared: PollOn[] = shared === true ? pollOnTypes : shared.map(toPollOn);
let realShared = shared === true ? ['unmoderated', 'modqueue', 'newComm', 'newSub'] : shared;
if (sharedMod === true) {
realShared.push(POLLING_UNMODERATED);
realShared.push(POLLING_MODQUEUE);
realShared.push('unmoderated');
realShared.push('modqueue');
}
const botLevelStatDefaults = {...statDefaultsFromOp, ...databaseStatisticsDefaults};
@@ -1570,7 +1566,7 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
caching: botCache,
userAgent,
polling: {
shared: Array.from(new Set(realShared)),
shared: [...new Set(realShared)] as PollOn[],
stagger,
limit,
interval,

View File

@@ -126,7 +126,28 @@ export class RecentActivityRule extends Rule {
async process(item: Submission | Comment): Promise<[boolean, RuleResult]> {
let activities;
// ACID is a bitch
// reddit may not return the activity being checked in the author's recent history due to availability/consistency issues or *something*
// so make sure we add it in if config is checking the same type and it isn't included
// TODO refactor this for SubredditState everywhere branch
let shouldIncludeSelf = true;
const strongWindow = windowConfigToWindowCriteria(this.window);
const {
filterOn: {
post: {
subreddits: {
include = [],
exclude = []
} = {},
} = {},
} = {}
} = strongWindow;
// typeof x === string -- a patch for now...technically this is all it supports but eventually will need to be able to do any SubredditState
if (include.length > 0 && !include.some(x => x.name !== undefined && x.name.toLocaleLowerCase() === item.subreddit.display_name.toLocaleLowerCase())) {
shouldIncludeSelf = false;
} else if (exclude.length > 0 && exclude.some(x => x.name !== undefined && x.name.toLocaleLowerCase() === item.subreddit.display_name.toLocaleLowerCase())) {
shouldIncludeSelf = false;
}
if(strongWindow.fetch === undefined && this.lookAt !== undefined) {
switch(this.lookAt) {
@@ -138,10 +159,25 @@ export class RecentActivityRule extends Rule {
}
}
// ACID is a bitch
// reddit may not return the activity being checked in the author's recent history due to availability/consistency issues or *something*
// so add current activity as a prefetched activity and add it to the returned activities (after it goes through filtering)
activities = await this.resources.getAuthorActivities(item.author, strongWindow, undefined, [item]);
activities = await this.resources.getAuthorActivities(item.author, strongWindow);
switch (strongWindow.fetch) {
case 'comment':
if (shouldIncludeSelf && item instanceof Comment && !activities.some(x => x.name === item.name)) {
activities.unshift(item);
}
break;
case 'submission':
if (shouldIncludeSelf && item instanceof Submission && !activities.some(x => x.name === item.name)) {
activities.unshift(item);
}
break;
default:
if (shouldIncludeSelf && !activities.some(x => x.name === item.name)) {
activities.unshift(item);
}
break;
}
let viableActivity = activities;
// if config does not specify reference then we set the default based on whether the item is a submission or not

View File

@@ -59,12 +59,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -83,9 +77,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -109,15 +100,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -139,9 +121,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -201,23 +180,6 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"banned": {
"anyOf": [
{
"$ref": "#/definitions/BanCriteria"
},
{
"items": {
"$ref": "#/definitions/BanCriteria"
},
"type": "array"
},
{
"type": "boolean"
}
],
"description": "Is the Author banned or not?\n\nIf user is not banned but BanCriteria(s) is present the test will fail\n\n* Use a boolean true/false for a simple yes or no\n* Or use a BanCriteria to test for specifics of an existing ban\n* Or use a list of BanCriteria -- if ANY BanCriteria passes the test passes\n\nNOTE: USE WITH CARE! This criteria usually incurs 1 API call"
},
"commentKarma": {
"description": "A string containing a comparison operator and a value to compare karma against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 comment karma\n* EX `<= 75%` => comment karma is less than or equal to 75% of **all karma**",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -383,12 +345,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -407,9 +363,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -456,15 +409,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -486,9 +430,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -537,50 +478,11 @@
],
"type": "object"
},
"BanCriteria": {
"properties": {
"bannedAt": {
"description": "Test when the Author was banned against this comparison\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if Author was banned more than 100 days ago\n* EX `<= 2 months` => Passes if Author was banned less than or equal to 2 months ago\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"daysLeft": {
"description": "Test how many days are left for Author's ban against this comparison\n\nIf the ban is permanent then the number of days left is equivalent to **INFINITY**\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if the Author's ban has more than 100 days left\n* EX `<= 2 months` => Passes if Author's ban has equal to or less than 2 months left\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "A string or list of strings to match ban note against.\n\nIf a list then ANY matched string makes this pass.\n\nString may be a regular expression enclosed in forward slashes. If it is not a regular expression then it is matched case-insensitive."
},
"permanent": {
"description": "Is the ban permanent?",
"type": "boolean"
}
},
"type": "object"
},
"CancelDispatchActionJson": {
"description": "Remove the Activity",
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -599,9 +501,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -641,15 +540,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -671,9 +561,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -726,18 +613,8 @@
"CommentActionJson": {
"description": "Reply to the Activity. For a submission the reply will be a top-level comment.",
"properties": {
"asModTeam": {
"description": "Comment \"as subreddit\" using the \"/u/subreddit-ModTeam\" account\n\nRESTRICTIONS:\n\n* Target activity must ALREADY BE REMOVED\n* Will always distinguish and sticky the created comment",
"type": "boolean"
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -756,9 +633,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -804,15 +678,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -834,9 +699,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -1107,12 +969,6 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1131,9 +987,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1157,15 +1010,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1187,9 +1031,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -1228,12 +1069,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1252,9 +1087,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1340,15 +1172,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1370,9 +1193,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -1605,12 +1425,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1629,9 +1443,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1663,15 +1474,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1693,9 +1495,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -1765,12 +1564,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1789,9 +1582,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1815,15 +1605,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1845,9 +1626,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -1882,12 +1660,6 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1906,9 +1678,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1950,15 +1719,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1980,9 +1740,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2146,12 +1903,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2170,9 +1921,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2220,15 +1968,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2250,9 +1989,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2486,12 +2222,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2510,9 +2240,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2536,15 +2263,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2566,9 +2284,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2611,12 +2326,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2635,9 +2344,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2670,15 +2376,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2700,9 +2397,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2733,12 +2427,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2757,9 +2445,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2818,15 +2503,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2848,9 +2524,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3212,12 +2885,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3236,9 +2903,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3270,15 +2934,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3300,9 +2955,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3345,12 +2997,6 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3369,9 +3015,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3419,15 +3062,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3449,9 +3083,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -24,23 +24,6 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"banned": {
"anyOf": [
{
"$ref": "#/definitions/BanCriteria"
},
{
"items": {
"$ref": "#/definitions/BanCriteria"
},
"type": "array"
},
{
"type": "boolean"
}
],
"description": "Is the Author banned or not?\n\nIf user is not banned but BanCriteria(s) is present the test will fail\n\n* Use a boolean true/false for a simple yes or no\n* Or use a BanCriteria to test for specifics of an existing ban\n* Or use a list of BanCriteria -- if ANY BanCriteria passes the test passes\n\nNOTE: USE WITH CARE! This criteria usually incurs 1 API call"
},
"commentKarma": {
"description": "A string containing a comparison operator and a value to compare karma against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 comment karma\n* EX `<= 75%` => comment karma is less than or equal to 75% of **all karma**",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -201,39 +184,6 @@
},
"type": "object"
},
"BanCriteria": {
"properties": {
"bannedAt": {
"description": "Test when the Author was banned against this comparison\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if Author was banned more than 100 days ago\n* EX `<= 2 months` => Passes if Author was banned less than or equal to 2 months ago\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"daysLeft": {
"description": "Test how many days are left for Author's ban against this comparison\n\nIf the ban is permanent then the number of days left is equivalent to **INFINITY**\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if the Author's ban has more than 100 days left\n* EX `<= 2 months` => Passes if Author's ban has equal to or less than 2 months left\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "A string or list of strings to match ban note against.\n\nIf a list then ANY matched string makes this pass.\n\nString may be a regular expression enclosed in forward slashes. If it is not a regular expression then it is matched case-insensitive."
},
"permanent": {
"description": "Is the ban permanent?",
"type": "boolean"
}
},
"type": "object"
},
"BotConnection": {
"description": "Configuration required to connect to a CM Server",
"properties": {

View File

@@ -432,10 +432,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -461,12 +461,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -485,9 +479,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -510,15 +501,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -540,9 +522,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -591,23 +570,6 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"banned": {
"anyOf": [
{
"$ref": "#/definitions/BanCriteria"
},
{
"items": {
"$ref": "#/definitions/BanCriteria"
},
"type": "array"
},
{
"type": "boolean"
}
],
"description": "Is the Author banned or not?\n\nIf user is not banned but BanCriteria(s) is present the test will fail\n\n* Use a boolean true/false for a simple yes or no\n* Or use a BanCriteria to test for specifics of an existing ban\n* Or use a list of BanCriteria -- if ANY BanCriteria passes the test passes\n\nNOTE: USE WITH CARE! This criteria usually incurs 1 API call"
},
"commentKarma": {
"description": "A string containing a comparison operator and a value to compare karma against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 comment karma\n* EX `<= 75%` => comment karma is less than or equal to 75% of **all karma**",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -772,12 +734,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -796,9 +752,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -833,15 +786,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -863,9 +807,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -891,39 +832,6 @@
],
"type": "object"
},
"BanCriteria": {
"properties": {
"bannedAt": {
"description": "Test when the Author was banned against this comparison\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if Author was banned more than 100 days ago\n* EX `<= 2 months` => Passes if Author was banned less than or equal to 2 months ago\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"daysLeft": {
"description": "Test how many days are left for Author's ban against this comparison\n\nIf the ban is permanent then the number of days left is equivalent to **INFINITY**\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if the Author's ban has more than 100 days left\n* EX `<= 2 months` => Passes if Author's ban has equal to or less than 2 months left\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "A string or list of strings to match ban note against.\n\nIf a list then ANY matched string makes this pass.\n\nString may be a regular expression enclosed in forward slashes. If it is not a regular expression then it is matched case-insensitive."
},
"permanent": {
"description": "Is the ban permanent?",
"type": "boolean"
}
},
"type": "object"
},
"CommentState": {
"description": "Different attributes a `Comment` can be in. Only include a property if you want to check it.",
"examples": [
@@ -1618,10 +1526,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1672,10 +1580,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1722,10 +1630,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1759,10 +1667,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1812,12 +1720,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1836,9 +1738,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1909,15 +1808,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1939,9 +1829,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2112,12 +1999,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2136,9 +2017,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2153,15 +2031,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2183,9 +2052,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2691,12 +2557,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2715,9 +2575,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2728,15 +2585,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2758,9 +2606,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2812,10 +2657,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -2957,10 +2802,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -2985,12 +2830,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3009,9 +2848,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3041,15 +2877,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3071,9 +2898,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3108,12 +2932,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3132,9 +2950,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3198,15 +3013,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3228,9 +3034,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3294,10 +3097,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3429,10 +3232,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3454,12 +3257,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3478,9 +3275,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3510,15 +3304,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3540,9 +3325,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3653,10 +3435,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3681,12 +3463,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3705,9 +3481,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3736,15 +3509,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3766,9 +3530,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"

View File

@@ -397,10 +397,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -426,12 +426,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -450,9 +444,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -475,15 +466,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -505,9 +487,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -556,23 +535,6 @@
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"banned": {
"anyOf": [
{
"$ref": "#/definitions/BanCriteria"
},
{
"items": {
"$ref": "#/definitions/BanCriteria"
},
"type": "array"
},
{
"type": "boolean"
}
],
"description": "Is the Author banned or not?\n\nIf user is not banned but BanCriteria(s) is present the test will fail\n\n* Use a boolean true/false for a simple yes or no\n* Or use a BanCriteria to test for specifics of an existing ban\n* Or use a list of BanCriteria -- if ANY BanCriteria passes the test passes\n\nNOTE: USE WITH CARE! This criteria usually incurs 1 API call"
},
"commentKarma": {
"description": "A string containing a comparison operator and a value to compare karma against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign]`\n\n* EX `> 100` => greater than 100 comment karma\n* EX `<= 75%` => comment karma is less than or equal to 75% of **all karma**",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
@@ -737,12 +699,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -761,9 +717,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -798,15 +751,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -828,9 +772,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -856,39 +797,6 @@
],
"type": "object"
},
"BanCriteria": {
"properties": {
"bannedAt": {
"description": "Test when the Author was banned against this comparison\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if Author was banned more than 100 days ago\n* EX `<= 2 months` => Passes if Author was banned less than or equal to 2 months ago\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"daysLeft": {
"description": "Test how many days are left for Author's ban against this comparison\n\nIf the ban is permanent then the number of days left is equivalent to **INFINITY**\n\nThe syntax is `(< OR > OR <= OR >=) <number> <unit>`\n\n* EX `> 100 days` => Passes if the Author's ban has more than 100 days left\n* EX `<= 2 months` => Passes if Author's ban has equal to or less than 2 months left\n\nUnit must be one of [DayJS Duration units](https://day.js.org/docs/en/durations/creating)\n\n[See] https://regexr.com/609n8 for example",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(days?|weeks?|months?|years?|hours?|minutes?|seconds?|milliseconds?)\\s*$",
"type": "string"
},
"note": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"description": "A string or list of strings to match ban note against.\n\nIf a list then ANY matched string makes this pass.\n\nString may be a regular expression enclosed in forward slashes. If it is not a regular expression then it is matched case-insensitive."
},
"permanent": {
"description": "Is the ban permanent?",
"type": "boolean"
}
},
"type": "object"
},
"CommentState": {
"description": "Different attributes a `Comment` can be in. Only include a property if you want to check it.",
"examples": [
@@ -1583,10 +1491,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1637,10 +1545,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1687,10 +1595,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1724,10 +1632,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -1777,12 +1685,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1801,9 +1703,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -1874,15 +1773,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1904,9 +1794,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2077,12 +1964,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2101,9 +1982,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2118,15 +1996,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2148,9 +2017,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2656,12 +2522,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2680,9 +2540,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -2693,15 +2550,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2723,9 +2571,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -2777,10 +2622,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -2922,10 +2767,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -2950,12 +2795,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2974,9 +2813,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3006,15 +2842,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3036,9 +2863,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3073,12 +2897,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3097,9 +2915,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3163,15 +2978,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3193,9 +2999,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3259,10 +3062,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3394,10 +3197,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3419,12 +3222,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3443,9 +3240,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3475,15 +3269,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3505,9 +3290,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"
@@ -3618,10 +3400,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"type": [
@@ -3646,12 +3428,6 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3670,9 +3446,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<AuthorCriteria>"
},
{
"type": "string"
}
],
"description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail."
@@ -3701,15 +3474,6 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3731,9 +3495,6 @@
},
{
"$ref": "#/definitions/FilterOptionsJson<TypedActivityState>"
},
{
"type": "string"
}
],
"description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]"

File diff suppressed because it is too large Load Diff

View File

@@ -93,8 +93,8 @@ import {EntityRunState} from "../Common/Entities/EntityRunState/EntityRunState";
import {
ActivitySourceValue,
EventRetentionPolicyRange,
Invokee, POLLING_COMMENTS, POLLING_MODQUEUE, POLLING_SUBMISSIONS, POLLING_UNMODERATED,
PollOn, pollOnTypes,
Invokee,
PollOn,
recordOutputTypes,
RunState
} from "../Common/Infrastructure/Atomic";
@@ -635,7 +635,7 @@ export class Manager extends EventEmitter implements RunningStates {
const configBuilder = new ConfigBuilder({logger: this.logger});
const validJson = configBuilder.validateJson(configObj);
const {
polling = [{pollOn: POLLING_SUBMISSIONS, limit: DEFAULT_POLLING_LIMIT, interval: DEFAULT_POLLING_INTERVAL}],
polling = [{pollOn: 'unmoderated', limit: DEFAULT_POLLING_LIMIT, interval: DEFAULT_POLLING_INTERVAL}],
caching,
credentials,
dryRun,
@@ -957,7 +957,7 @@ export class Manager extends EventEmitter implements RunningStates {
await this.resources.setActivityLastSeenDate(item.name);
// if modqueue is running then we know we are checking for new reports every X seconds
if(options.activitySource.identifier === POLLING_MODQUEUE) {
if(options.activitySource.identifier === 'modqueue') {
// if the activity is from modqueue and only has one report then we know that report was just created
if(item.num_reports === 1
// otherwise if it has more than one report AND we have seen it (its only seen if it has already been stored (in below block))
@@ -975,7 +975,7 @@ export class Manager extends EventEmitter implements RunningStates {
let shouldPersistReports = false;
if (existingEntity === null) {
activityEntity = Activity.fromSnoowrapActivity(this.managerEntity.subreddit, activity, lastKnownStateTimestamp);
activityEntity = await Activity.fromSnoowrapActivity(activity, {lastKnownStateTimestamp, db: this.resources.database});
// always persist if activity is not already persisted and any reports exist
if (item.num_reports > 0) {
shouldPersistReports = true;
@@ -1189,7 +1189,7 @@ export class Manager extends EventEmitter implements RunningStates {
// @ts-ignore
const subProxy = await this.client.getSubmission((item as Comment).link_id);
const sub = await this.resources.getActivity(subProxy);
subActivity = await this.activityRepo.save(Activity.fromSnoowrapActivity(this.managerEntity.subreddit, sub));
subActivity = await this.activityRepo.save(await Activity.fromSnoowrapActivity(sub, {db: this.resources.database}));
}
event.activity.submission = subActivity;
@@ -1325,20 +1325,25 @@ export class Manager extends EventEmitter implements RunningStates {
}
}
isPollingShared(streamName: PollOn): boolean {
isPollingShared(streamName: string): boolean {
const pollOption = this.pollOptions.find(x => x.pollOn === streamName);
return pollOption !== undefined && pollOption.limit === DEFAULT_POLLING_LIMIT && pollOption.interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(streamName);
return pollOption !== undefined && pollOption.limit === DEFAULT_POLLING_LIMIT && pollOption.interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(streamName as PollOn);
}
async buildPolling() {
const sources = [...pollOnTypes];
const sources: PollOn[] = ['unmoderated', 'modqueue', 'newComm', 'newSub'];
const subName = this.subreddit.display_name;
for (const source of sources) {
const pollOpt = this.pollOptions.find(x => x.pollOn === source);
if (!sources.includes(source)) {
this.logger.error(`'${source}' is not a valid polling source. Valid sources: unmoderated | modqueue | newComm | newSub`);
continue;
}
const pollOpt = this.pollOptions.find(x => x.pollOn.toLowerCase() === source.toLowerCase());
if (pollOpt === undefined) {
if(this.sharedStreamCallbacks.has(source)) {
this.logger.debug(`Removing listener for shared polling on ${source.toUpperCase()} because it no longer exists in config`);
@@ -1361,11 +1366,11 @@ export class Manager extends EventEmitter implements RunningStates {
let modStreamType: string | undefined;
switch (source) {
case POLLING_UNMODERATED:
case 'unmoderated':
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = POLLING_UNMODERATED;
modStreamType = 'unmoderated';
// use default mod stream from resources
stream = this.cacheManager.modStreams.get(POLLING_UNMODERATED) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get('unmoderated') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new UnmoderatedStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1375,11 +1380,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case POLLING_MODQUEUE:
case 'modqueue':
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = POLLING_MODQUEUE;
modStreamType = 'modqueue';
// use default mod stream from resources
stream = this.cacheManager.modStreams.get(POLLING_MODQUEUE) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get('modqueue') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new ModQueueStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1389,11 +1394,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case POLLING_SUBMISSIONS:
case 'newSub':
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = POLLING_SUBMISSIONS;
modStreamType = 'newSub';
// use default mod stream from resources
stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get('newSub') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new SubmissionStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1403,11 +1408,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case POLLING_COMMENTS:
case 'newComm':
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = POLLING_COMMENTS;
modStreamType = 'newComm';
// use default mod stream from resources
stream = this.cacheManager.modStreams.get(POLLING_COMMENTS) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get('newComm') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new CommentStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1417,8 +1422,6 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
default:
throw new CMError(`This shouldn't happen! All polling sources are enumerated in switch. Source value: ${source}`)
}
if (stream === undefined) {
@@ -1511,10 +1514,10 @@ export class Manager extends EventEmitter implements RunningStates {
}
noChecksWarning = (source: PollOn) => (listing: any) => {
if (this.commentChecks.length === 0 && [POLLING_MODQUEUE, POLLING_COMMENTS].some(x => x === source)) {
if (this.commentChecks.length === 0 && ['modqueue', 'newComm'].some(x => x === source)) {
this.logger.warn(`Polling '${source.toUpperCase()}' may return Comments but no comments checks were configured.`);
}
if (this.submissionChecks.length === 0 && [POLLING_UNMODERATED, POLLING_MODQUEUE, POLLING_SUBMISSIONS].some(x => x === source)) {
if (this.submissionChecks.length === 0 && ['unmoderated', 'modqueue', 'newSub'].some(x => x === source)) {
this.logger.warn(`Polling '${source.toUpperCase()}' may return Submissions but no submission checks were configured.`);
}
}
@@ -1667,7 +1670,7 @@ export class Manager extends EventEmitter implements RunningStates {
}
this.startedAt = dayjs();
const modQueuePollOpts = this.pollOptions.find(x => x.pollOn === POLLING_MODQUEUE);
const modQueuePollOpts = this.pollOptions.find(x => x.pollOn === 'modqueue');
if(modQueuePollOpts !== undefined) {
this.modqueueInterval = modQueuePollOpts.interval;
}

View File

@@ -29,7 +29,7 @@ import {
generateFullWikiUrl,
generateItemFilterHelpers,
getActivityAuthorName,
getActivitySubredditName, humanizeBanDetails,
getActivitySubredditName,
isComment,
isCommentState,
isRuleSetResult,
@@ -56,7 +56,7 @@ import {
} from "../util";
import {
ActivityDispatch,
CacheConfig, CacheOptions,
CacheConfig,
Footer,
HistoricalStatsDisplay,
ResourceStats, StrongTTLConfig,
@@ -69,7 +69,16 @@ import {cacheTTLDefaults, createHistoricalDisplayDefaults,} from "../Common/defa
import {ExtendedSnoowrap} from "../Utils/SnoowrapClients";
import dayjs, {Dayjs} from "dayjs";
import ImageData from "../Common/ImageData";
import {Between, DataSource, DeleteQueryBuilder, LessThan, Repository, SelectQueryBuilder} from "typeorm";
import {
Between, Brackets,
DataSource,
DeleteQueryBuilder,
In,
LessThan,
NotBrackets,
Repository,
SelectQueryBuilder
} from "typeorm";
import {CMEvent as ActionedEventEntity, CMEvent} from "../Common/Entities/CMEvent";
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
import globrex from 'globrex';
@@ -88,7 +97,7 @@ import cloneDeep from "lodash/cloneDeep";
import {
asModLogCriteria,
asModNoteCriteria,
AuthorCriteria, BanCriteria,
AuthorCriteria,
cmToSnoowrapActivityMap, cmToSnoowrapAuthorMap,
CommentState,
ModLogCriteria,
@@ -104,7 +113,7 @@ import {
UserNoteCriteria
} from "../Common/Infrastructure/Filters/FilterCriteria";
import {
ActivitySourceValue, asSubredditPlaceholder,
ActivitySourceValue,
ConfigFragmentParseFunc,
DurationVal,
EventRetentionPolicyRange,
@@ -115,7 +124,7 @@ import {
ModUserNoteLabel,
RelativeDateTimeMatch,
statFrequencies,
StatisticFrequencyOption, SubredditPlaceholderType,
StatisticFrequencyOption,
WikiContext
} from "../Common/Infrastructure/Atomic";
import {
@@ -136,9 +145,9 @@ import {Duration} from "dayjs/plugin/duration";
import {
ActivityType,
AuthorHistorySort,
CachedFetchedActivitiesResult, CMBannedUser,
CachedFetchedActivitiesResult,
FetchedActivitiesResult, MaybeActivityType, RedditUserLike,
SnoowrapActivity, SnoowrapBannedUser,
SnoowrapActivity,
SubredditLike,
SubredditRemovalReason
} from "../Common/Infrastructure/Reddit";
@@ -161,9 +170,9 @@ import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
import {ActivitySource} from "../Common/ActivitySource";
import {SubredditResourceOptions} from "../Common/Subreddit/SubredditResourceInterfaces";
import {SubredditStats} from "./Stats";
import {CMCache, createCacheManager} from "../Common/Cache";
import {BannedUser, BanOptions} from "snoowrap/dist/objects/Subreddit";
import {testBanCriteria} from "../Utils/Criteria/AuthorCritUtils";
import {CMCache} from "../Common/Cache";
import { Activity } from '../Common/Entities/Activity';
import {FindOptionsWhere} from "typeorm/find-options/FindOptionsWhere";
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 have any ideas, questions, or concerns about this action.';
@@ -189,13 +198,13 @@ export class SubredditResources {
database: DataSource
client: ExtendedSnoowrap
cache: CMCache
memoryCache: CMCache
cacheSettingsHash?: string;
thirdPartyCredentials: ThirdPartyCredentialsJsonConfig;
delayedItems: ActivityDispatch[] = [];
botAccount?: string;
dispatchedActivityRepo: Repository<DispatchedEntity>
activitySourceRepo: Repository<ActivitySourceEntity>
activityRepo: Repository<Activity>
retention?: EventRetentionPolicyRange
managerEntity: ManagerEntity
botEntity: Bot
@@ -232,6 +241,7 @@ export class SubredditResources {
this.database = database;
this.dispatchedActivityRepo = this.database.getRepository(DispatchedEntity);
this.activitySourceRepo = this.database.getRepository(ActivitySourceEntity);
this.activityRepo = this.database.getRepository(Activity);
this.retention = retention;
//this.prefix = prefix;
this.client = client;
@@ -247,12 +257,6 @@ export class SubredditResources {
}
this.cache = cache;
this.cache.setLogger(this.logger);
const memoryCacheOpts: CacheOptions = {
store: 'memory',
max: 10,
ttl: 10
};
this.memoryCache = new CMCache(createCacheManager(memoryCacheOpts), memoryCacheOpts, false, undefined, {}, this.logger);
this.subredditStats = new SubredditStats(database, managerEntity, cache, statFrequency, this.logger);
@@ -413,21 +417,25 @@ export class SubredditResources {
}
},
relations: {
manager: true
manager: true,
activity: {
submission: true
}
}
});
const now = dayjs();
const toRemove = [];
for(const dAct of dispatchedActivities) {
const shouldDispatchAt = dAct.createdAt.add(dAct.delay.asSeconds(), 'seconds');
let tardyHint = '';
if(shouldDispatchAt.isBefore(now)) {
let tardyHint = `Activity ${dAct.activityId} queued at ${dAct.createdAt.format('YYYY-MM-DD HH:mm:ssZ')} for ${dAct.delay.humanize()} is now LATE`;
let tardyHint = `Activity ${dAct.activity.id} queued at ${dAct.createdAt.format('YYYY-MM-DD HH:mm:ssZ')} for ${dAct.delay.humanize()} is now LATE`;
if(dAct.tardyTolerant === true) {
tardyHint += ` but was configured as ALWAYS 'tardy tolerant' so will be dispatched immediately`;
} else if(dAct.tardyTolerant === false) {
tardyHint += ` and was not configured as 'tardy tolerant' so will be dropped`;
this.logger.warn(tardyHint);
await this.removeDelayedActivity(dAct.id);
toRemove.push(dAct.id);
continue;
} else {
// see if its within tolerance
@@ -435,7 +443,7 @@ export class SubredditResources {
if(latest.isBefore(now)) {
tardyHint += ` and IS NOT within tardy tolerance of ${dAct.tardyTolerant.humanize()} of planned dispatch time so will be dropped`;
this.logger.warn(tardyHint);
await this.removeDelayedActivity(dAct.id);
toRemove.push(dAct.id);
continue;
} else {
tardyHint += `but is within tardy tolerance of ${dAct.tardyTolerant.humanize()} of planned dispatch time so will be dispatched immediately`;
@@ -448,27 +456,115 @@ export class SubredditResources {
try {
this.delayedItems.push(await dAct.toActivityDispatch(this.client))
} catch (e) {
this.logger.warn(new ErrorWithCause(`Unable to add Activity ${dAct.activityId} from database delayed activities to in-app delayed activities queue`, {cause: e}));
this.logger.warn(new ErrorWithCause(`Unable to add Activity ${dAct.activity.id} from database delayed activities to in-app delayed activities queue`, {cause: e}));
}
}
if(toRemove.length > 0) {
await this.removeDelayedActivity(toRemove);
}
}
}
async addDelayedActivity(data: ActivityDispatch) {
const dEntity = await this.dispatchedActivityRepo.save(new DispatchedEntity({...data, manager: this.managerEntity}));
// TODO merge this with getActivity or something...
if(asComment(data.activity)) {
const existingSub = await this.activityRepo.findOneBy({_id: data.activity.link_id});
if(existingSub === null) {
const sub = await this.getActivity(new Submission({name: data.activity.link_id}, this.client, false));
await this.activityRepo.save(await Activity.fromSnoowrapActivity(sub, {db: this.database}));
}
}
const dEntity = await this.dispatchedActivityRepo.save(new DispatchedEntity({...data, manager: this.managerEntity, activity: await Activity.fromSnoowrapActivity(data.activity, {db: this.database})}));
data.id = dEntity.id;
this.delayedItems.push(data);
}
async removeDelayedActivity(val?: string | string[]) {
if(val === undefined) {
await this.dispatchedActivityRepo.delete({manager: {id: this.managerEntity.id}});
this.delayedItems = [];
} else {
let dispatched: DispatchedEntity[] = [];
const where: FindOptionsWhere<DispatchedEntity> = {
manager: {
id: this.managerEntity.id
}
};
if(val !== undefined) {
const ids = typeof val === 'string' ? [val] : val;
await this.dispatchedActivityRepo.delete(ids);
this.delayedItems = this.delayedItems.filter(x => !ids.includes(x.id));
where.id = In(ids);
}
dispatched = await this.dispatchedActivityRepo.find({
where,
relations: {
manager: true,
activity: {
actionedEvents: true,
submission: {
actionedEvents: true
}
}
}
});
const actualDispatchedIds = dispatched.map(x => x.id);
this.logger.debug(`${actualDispatchedIds.length} marked for deletion`, {leaf: 'Delayed Activities'});
// get potential activities to delete
// but only include activities that don't have any actionedEvents
let activityIdsToDelete = Array.from(dispatched.reduce((acc, curr) => {
if(curr.activity.actionedEvents === null || curr.activity.actionedEvents.length === 0) {
acc.add(curr.activity.id);
}
if(curr.activity.submission !== undefined && curr.activity.submission !== null) {
if(curr.activity.submission.actionedEvents === null || curr.activity.submission.actionedEvents.length === 0) {
acc.add(curr.activity.submission.id);
}
}
return acc;
}, new Set<string>()));
const rawActCount = activityIdsToDelete.length;
let activeActCount = 0;
// if we have any potential activities to delete we now need to get any dispatched actions that reference these activities
// that are NOT the ones we are going to delete
if(activityIdsToDelete.length > 0) {
const activeDispatchedQuery = this.dispatchedActivityRepo.createQueryBuilder('dis')
.leftJoinAndSelect('dis.activity', 'activity')
.leftJoinAndSelect('activity.submission', 'submission')
.where(new NotBrackets((qb) => {
qb.where('dis.id IN (:...currIds)', {currIds: actualDispatchedIds});
}))
.andWhere(new Brackets((qb) => {
qb.where('activity._id IN (:...actMainIds)', {actMainIds: activityIdsToDelete})
qb.orWhere('submission._id IN (:...actSubIds)', {actSubIds: activityIdsToDelete})
}));
//const sql = activeDispatchedQuery.getSql();
const activeDispatched = await activeDispatchedQuery.getMany();
// all activity ids, from the actions to delete, that are being used by dispatched actions that are NOT the ones we are going to delete
const activeDispatchedIds = Array.from(activeDispatched.reduce((acc, curr) => {
acc.add(curr.activity.id);
if(curr.activity.submission !== undefined && curr.activity.submission !== null) {
acc.add(curr.activity.submission.id);
}
return acc;
}, new Set<string>()));
activeActCount = activeDispatchedIds.length;
// filter out any that are still in use
activityIdsToDelete = activityIdsToDelete.filter(x => !activeDispatchedIds.includes(x));
}
this.logger.debug(`Marked ${activityIdsToDelete.length} Activities created, by Delayed, for deletion (${rawActCount} w/o Events | ${activeActCount} used by other Delayed Activities)`, {leaf: 'Delayed Activities'});
if(actualDispatchedIds.length > 0) {
await this.dispatchedActivityRepo.delete(actualDispatchedIds);
} else {
this.logger.warn('No dispatched ids found to delete');
}
if(activityIdsToDelete.length > 0) {
await this.activityRepo.delete(activityIdsToDelete);
}
this.delayedItems = this.delayedItems.filter(x => !actualDispatchedIds.includes(x.id));
}
async initStats() {
@@ -870,47 +966,6 @@ export class SubredditResources {
}
}
async getSubredditBannedUser(val: string | RedditUser): Promise<CMBannedUser | undefined> {
const subName = this.subreddit.display_name;
const name = getActivityAuthorName(val);
const hash = `sub-${subName}-banned-${name}`;
if (this.ttl.authorTTL !== false) {
const cachedBanData = (await this.cache.get(hash)) as undefined | null | false | SnoowrapBannedUser;
if (cachedBanData !== undefined && cachedBanData !== null) {
this.logger.debug(`Cache Hit: Subreddit Banned User ${subName} ${name}`);
if(cachedBanData === false) {
return undefined;
}
return {...cachedBanData, date: dayjs.unix(cachedBanData.date), days_left: cachedBanData.days_left === null ? undefined : dayjs.duration({days: cachedBanData.days_left}), user: new RedditUser({name: cachedBanData.name}, this.client, false)};
}
}
let bannedUsers = await this.subreddit.getBannedUsers({name});
let bannedUser: CMBannedUser | undefined;
if(bannedUsers.length > 0) {
const banData = bannedUsers[0] as SnoowrapBannedUser;
bannedUser = {...banData, date: dayjs.unix(banData.date), days_left: banData.days_left === null ? undefined : dayjs.duration({days: banData.days_left}), user: new RedditUser({name: banData.name}, this.client, false)};
}
if (this.ttl.authorTTL !== false) {
// @ts-ignore
await this.cache.set(hash, bannedUsers.length > 0 ? bannedUsers[0] as SnoowrapBannedUser : false, {ttl: this.ttl.subredditTTL});
}
return bannedUser;
}
async addUserToSubredditBannedUserCache(data: BanOptions) {
if (this.ttl.authorTTL !== false) {
const subName = this.subreddit.display_name;
const name = getActivityAuthorName(data.name);
const hash = `sub-${subName}-banned-${name}`;
const banData: SnoowrapBannedUser = {date: dayjs().unix(), name: data.name, days_left: data.duration ?? null, note: data.banNote ?? ''};
await this.cache.set(hash, banData, {ttl: this.ttl.authorTTL})
}
}
async hasSubreddit(name: string) {
if (this.ttl.subredditTTL !== false) {
const hash = `sub-${name}`;
@@ -1080,13 +1135,13 @@ export class SubredditResources {
}
}
async getAuthorActivities(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing, prefetchedActivities?: SnoowrapActivity[]): Promise<SnoowrapActivity[]> {
async getAuthorActivities(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing): Promise<SnoowrapActivity[]> {
const {post} = await this.getAuthorActivitiesWithFilter(user, options, customListing, prefetchedActivities);
const {post} = await this.getAuthorActivitiesWithFilter(user, options, customListing);
return post;
}
async getAuthorActivitiesWithFilter(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing, prefetchedActivities?: SnoowrapActivity[]): Promise<FetchedActivitiesResult> {
async getAuthorActivitiesWithFilter(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing): Promise<FetchedActivitiesResult> {
let listFuncName: string;
let listFunc: ListingFunc;
@@ -1114,24 +1169,21 @@ export class SubredditResources {
...(cloneDeep(options)),
}
return await this.getActivities(user, criteriaWithDefaults, {func: listFunc, name: listFuncName}, prefetchedActivities);
return await this.getActivities(user, criteriaWithDefaults, {func: listFunc, name: listFuncName});
}
async getAuthorComments(user: RedditUser, options: ActivityWindowCriteria, prefetchedActivities?: SnoowrapActivity[]): Promise<Comment[]> {
return await this.getAuthorActivities(user, {...options, fetch: 'comment'}, undefined, prefetchedActivities) as unknown as Promise<Comment[]>;
async getAuthorComments(user: RedditUser, options: ActivityWindowCriteria): Promise<Comment[]> {
return await this.getAuthorActivities(user, {...options, fetch: 'comment'}) as unknown as Promise<Comment[]>;
}
async getAuthorSubmissions(user: RedditUser, options: ActivityWindowCriteria, prefetchedActivities?: SnoowrapActivity[]): Promise<Submission[]> {
async getAuthorSubmissions(user: RedditUser, options: ActivityWindowCriteria): Promise<Submission[]> {
return await this.getAuthorActivities(user, {
...options,
fetch: 'submission'
}, undefined,prefetchedActivities) as unknown as Promise<Submission[]>;
}) as unknown as Promise<Submission[]>;
}
async getActivities(user: RedditUser, options: ActivityWindowCriteria, listingData: NamedListing, prefetchedActivities: SnoowrapActivity[] = []): Promise<FetchedActivitiesResult> {
let cacheKey: string | undefined;
let fromCache = false;
async getActivities(user: RedditUser, options: ActivityWindowCriteria, listingData: NamedListing): Promise<FetchedActivitiesResult> {
try {
@@ -1140,6 +1192,7 @@ export class SubredditResources {
let apiCount = 1;
let preMaxTrigger: undefined | string;
let rawCount: number = 0;
let fromCache = false;
const hashObj = cloneDeep(options);
@@ -1152,23 +1205,13 @@ export class SubredditResources {
const userName = getActivityAuthorName(user);
const hash = objectHash.sha1(hashObj);
cacheKey = `${userName}-${listingData.name}-${hash}`;
const cacheKey = `${userName}-${listingData.name}-${hash}`;
if (this.ttl.authorTTL !== false) {
if (this.useSubredditAuthorCache) {
hashObj.subreddit = this.subreddit;
}
// check for cached request error!
//
// we cache reddit API request errors for 403/404 (suspended/shadowban) in memory so that
// we don't waste API calls making the same call repetitively since we know what the result will always be
const cachedRequestError = await this.memoryCache.get(cacheKey) as undefined | null | Error;
if(cachedRequestError !== undefined && cachedRequestError !== null) {
fromCache = true;
this.logger.debug(`In-memory cache found reddit request error for key ${cacheKey}. Must have been <5 sec ago. Throwing to save API calls!`);
throw cachedRequestError;
}
const cacheVal = await this.cache.get(cacheKey);
if(cacheVal === undefined || cacheVal === null) {
@@ -1275,24 +1318,12 @@ export class SubredditResources {
}
}
let preFilteredPrefetchedActivities = [...prefetchedActivities];
if(preFilteredPrefetchedActivities.length > 0) {
switch(options.fetch) {
// TODO this may not work if using a custom listingFunc that does not include fetch type
case 'comment':
preFilteredPrefetchedActivities = preFilteredPrefetchedActivities.filter(x => asComment(x));
break;
case 'submission':
preFilteredPrefetchedActivities = preFilteredPrefetchedActivities.filter(x => asSubmission(x));
break;
}
preFilteredPrefetchedActivities = await this.filterListingWithHistoryOptions(preFilteredPrefetchedActivities, user, options.filterOn?.pre);
}
let unFilteredItems: SnoowrapActivity[] | undefined = [...preFilteredPrefetchedActivities];
pre = pre.concat(preFilteredPrefetchedActivities);
let unFilteredItems: SnoowrapActivity[] | undefined;
const { func: listingFunc } = listingData;
let listing = await listingFunc(getAuthorHistoryAPIOptions(options));
let hitEnd = false;
let offset = chunkSize;
@@ -1302,9 +1333,6 @@ export class SubredditResources {
timeOk = false;
let listSlice = listing.slice(offset - chunkSize);
// filter out any from slice that were already included from prefetched list so that prefetched aren't included twice
listSlice = preFilteredPrefetchedActivities.length === 0 ? listSlice : listSlice.filter(x => !preFilteredPrefetchedActivities.some(y => y.name === x.name));
let preListSlice = await this.filterListingWithHistoryOptions(listSlice, user, options.filterOn?.pre);
// its more likely the time criteria is going to be hit before the count criteria
@@ -1405,14 +1433,9 @@ export class SubredditResources {
} catch (err: any) {
if(isStatusError(err)) {
switch(err.statusCode) {
case 403:
case 404:
if(!fromCache && cacheKey !== undefined) {
await this.memoryCache.set(cacheKey, err, {ttl: 5});
}
if(err.statusCode === 404) {
throw new SimpleError('Reddit returned a 404 for user history. Likely this user is shadowbanned.', {isSerious: false});
}
throw new SimpleError('Reddit returned a 404 for user history. Likely this user is shadowbanned.', {isSerious: false});
case 403:
throw new MaybeSeriousErrorWithCause('Reddit returned a 403 for user history, likely this user is suspended.', {cause: err, isSerious: false});
default:
throw err;
@@ -1584,7 +1607,6 @@ export class SubredditResources {
usernotes,
ruleResults,
actionResults,
author: (val) => this.getAuthor(val)
});
}
@@ -1958,14 +1980,8 @@ export class SubredditResources {
if (crit[k] !== undefined) {
switch (k) {
case 'name':
const nameReg = crit[k] as RegExp | SubredditPlaceholderType;
// placeholder {{subreddit}} tests as true if the given subreddit matches the subreddit this bot is processing the activity from
if (asSubredditPlaceholder(nameReg)) {
if (this.subreddit.display_name !== subreddit.display_name) {
log.debug(`Failed: Expected => ${k}:${crit[k]} (${this.subreddit.display_name}) | Found => ${k}:${subreddit.display_name}`)
return false
}
} else if (!nameReg.test(subreddit.display_name)) {
const nameReg = crit[k] as RegExp;
if(!nameReg.test(subreddit.display_name)) {
return false;
}
break;
@@ -2788,33 +2804,6 @@ export class SubredditResources {
shouldContinue = false;
}
break;
case 'banned':
const banDetails = await this.getSubredditBannedUser(item.author);
const isBanned = banDetails !== undefined;
propResultsMap.banned!.found = humanizeBanDetails(banDetails);
if(typeof authorOpts.banned === 'boolean') {
propResultsMap.banned!.passed = criteriaPassWithIncludeBehavior(isBanned === authorOpts.banned, include);
} else if(!isBanned) {
// since banned criteria is not boolean it must be criteria(s)
// and if user is not banned then no criteria will pass
propResultsMap.banned!.passed = criteriaPassWithIncludeBehavior(false, include);
} else {
const bCritVal = authorOpts.banned as BanCriteria | BanCriteria[];
const bCritArr = !Array.isArray(bCritVal) ? [bCritVal] : bCritVal;
let anyBanCritPassed = false;
for(const bCrit of bCritArr) {
anyBanCritPassed = testBanCriteria(bCrit, banDetails);
if(anyBanCritPassed) {
break;
}
}
propResultsMap.banned!.passed = criteriaPassWithIncludeBehavior(anyBanCritPassed, include);
}
if (!propResultsMap.banned!.passed) {
shouldContinue = false;
}
break;
case 'userNotes':
const unCriterias = (authorOpts[k] as UserNoteCriteria[]).map(x => toFullUserNoteCriteria(x));
const notes = await this.userNotes.getUserNotes(item.author);

View File

@@ -1,79 +0,0 @@
import {BanCriteria} from "../../Common/Infrastructure/Filters/FilterCriteria";
import {boolToString, testMaybeStringRegex} from "../../util";
import {CMBannedUser} from "../../Common/Infrastructure/Reddit";
import {compareDurationValue, parseDurationComparison} from "../../Common/Infrastructure/Comparisons";
import dayjs from "dayjs";
export const humanizeBanCriteria = (crit: BanCriteria): string => {
const parts: string[] = [];
for (const [k, v] of Object.entries(crit)) {
switch (k.toLowerCase()) {
case 'note':
parts.push(`has notes matching: "${Array.isArray(v) ? v.join(' || ') : v}"`);
break;
default:
parts.push(`${k}: ${typeof v === 'boolean' ? boolToString(v) : v.toString()}`);
break;
}
}
return parts.join(' AND ');
}
export const testBanCriteria = (crit: BanCriteria, banUser: CMBannedUser): boolean => {
if (crit.permanent !== undefined) {
// easiest to test for
if ((banUser.days_left === undefined && !crit.permanent) || (banUser.days_left !== undefined && crit.permanent)) {
return false;
}
}
if (crit.note !== undefined) {
let anyPassed = false;
const expectedValues = Array.isArray(crit.note) ? crit.note : [crit.note];
for (const expectedVal of expectedValues) {
try {
const [regPassed] = testMaybeStringRegex(expectedVal, banUser.note);
if (regPassed) {
anyPassed = true;
}
} catch (err: any) {
if (err.message.includes('Could not convert test value')) {
// fallback to simple comparison
anyPassed = expectedVal.toLowerCase() === banUser.note.toLowerCase();
} else {
throw err;
}
}
if (anyPassed) {
break;
}
}
if (!anyPassed) {
return false;
}
}
if (crit.bannedAt !== undefined) {
const ageTest = compareDurationValue(parseDurationComparison(crit.bannedAt), banUser.date);
if (!ageTest) {
return false;
}
}
if (crit.daysLeft !== undefined) {
const daysLeftCompare = parseDurationComparison(crit.daysLeft);
if (banUser.days_left === undefined) {
if (daysLeftCompare.operator.includes('<')) {
// permaban, will never be less than some finite duration
return false;
}
// otherwise will always pass since any finite duration is less than infinity
} else {
const dayTest = compareDurationValue(daysLeftCompare, dayjs().add(banUser.days_left));
if (!dayTest) {
return false;
}
}
}
return true;
}

View File

@@ -46,6 +46,83 @@ import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
export const BOT_LINK = 'https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot';
export interface AuthorTypedActivitiesOptions extends ActivityWindowCriteria {
type?: 'comment' | 'submission',
}
export const isSubreddit = async (subreddit: Subreddit, stateCriteria: SubredditCriteria | StrongSubredditCriteria, logger?: Logger) => {
delete stateCriteria.stateDescription;
if (Object.keys(stateCriteria).length === 0) {
return true;
}
const crit = isStrongSubredditState(stateCriteria) ? stateCriteria : toStrongSubredditState(stateCriteria, {defaultFlags: 'i'});
const log: Logger | undefined = logger !== undefined ? logger.child({leaf: 'Subreddit Check'}, mergeArr) : undefined;
return await (async () => {
for (const k of Object.keys(crit)) {
// @ts-ignore
if (crit[k] !== undefined) {
switch (k) {
case 'name':
const nameReg = crit[k] as RegExp;
if(!nameReg.test(subreddit.display_name)) {
return false;
}
break;
case 'isUserProfile':
const entity = parseRedditEntity(subreddit.display_name);
const entityIsUserProfile = entity.type === 'user';
if(crit[k] !== entityIsUserProfile) {
if(log !== undefined) {
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${k}:${entityIsUserProfile}`)
}
return false
}
break;
case 'over18':
case 'over_18':
// handling an edge case where user may have confused Comment/Submission state "over_18" with SubredditState "over18"
// @ts-ignore
if (crit[k] !== subreddit.over18) {
if(log !== undefined) {
// @ts-ignore
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${k}:${subreddit.over18}`)
}
return false
}
break;
default:
// @ts-ignore
if (subreddit[k] !== undefined) {
// @ts-ignore
if (crit[k] !== subreddit[k]) {
if(log !== undefined) {
// @ts-ignore
log.debug(`Failed: Expected => ${k}:${crit[k]} | Found => ${k}:${subreddit[k]}`)
}
return false
}
} else {
if(log !== undefined) {
log.warn(`Tried to test for Subreddit property '${k}' but it did not exist`);
}
}
break;
}
}
}
if(log !== undefined) {
log.debug(`Passed: ${JSON.stringify(stateCriteria)}`);
}
return true;
})() as boolean;
}
const renderContentCommentTruncate = truncateStringToLength(50);
const shortTitleTruncate = truncateStringToLength(15);
@@ -56,7 +133,6 @@ export interface TemplateContext {
ruleResults?: RuleResultEntity[]
actionResults?: ActionResultEntity[]
activity?: SnoowrapActivity
author?: (val: string | RedditUser) => Promise<RedditUser>
[key: string]: any
}
@@ -64,25 +140,11 @@ export const renderContent = async (template: string, data: TemplateContext = {}
const {
usernotes,
ruleResults,
author,
actionResults,
activity,
...restContext
} = data;
let fetchedUser: RedditUser | undefined;
// @ts-ignore
const user = async (): Promise<RedditUser> => {
if(fetchedUser === undefined) {
if(author !== undefined) {
// @ts-ignore
fetchedUser = await author(activity.author);
}
}
// @ts-ignore
return fetchedUser;
}
let view: GenericContentTemplateData = {
botLink: BOT_LINK,
...restContext
@@ -100,7 +162,6 @@ export const renderContent = async (template: string, data: TemplateContext = {}
conditional.spoiler = activity.spoiler;
conditional.op = true;
conditional.upvoteRatio = `${activity.upvote_ratio * 100}%`;
conditional.link_flair_text = activity.link_flair_text;
} else {
conditional.op = activity.is_submitter;
}
@@ -110,25 +171,10 @@ export const renderContent = async (template: string, data: TemplateContext = {}
view.modmailLink = `https://www.reddit.com/message/compose?to=%2Fr%2F${subreddit}&message=${encodeURIComponent(permalink)}`;
const author: any = {
toString: () => getActivityAuthorName(activity.author)
};
if(template.includes('{{item.author.')) {
// @ts-ignore
const auth = await user();
author.age = dayjs.unix(auth.created).fromNow(true);
author.linkKarma = auth.link_karma;
author.commentKarma = auth.comment_karma;
author.totalKarma = auth.comment_karma + auth.link_karma;
author.verified = auth.has_verified_email;
author.flairText = activity.author_flair_text;
}
const templateData: any = {
kind: activity instanceof Submission ? 'submission' : 'comment',
author,
// @ts-ignore
author: getActivityAuthorName(await activity.author),
votes: activity.score,
age: dayjs.duration(dayjs().diff(dayjs.unix(activity.created))).humanize(),
permalink,

View File

@@ -12,7 +12,6 @@ import {Logger} from "winston";
import {WebSetting} from "../../Common/WebEntities/WebSetting";
import {ErrorWithCause} from "pony-cause";
import {createCacheManager} from "../../Common/Cache";
import {MysqlDriver} from "typeorm/driver/mysql/MysqlDriver";
export interface CacheManagerStoreOptions {
prefix?: string
@@ -104,12 +103,7 @@ export class DatabaseStorageProvider extends StorageProvider {
}
createSessionStore(options?: TypeormStoreOptions): Store {
// https://github.com/freshgiammi-lab/connect-typeorm#implement-the-session-entity
// https://github.com/freshgiammi-lab/connect-typeorm/issues/8
// usage of LIMIT in subquery is not supported by mariadb/mysql
// limitSubquery: false -- turns off LIMIT usage
const realOptions = this.database.driver instanceof MysqlDriver ? {...options, limitSubquery: false} : options;
return new TypeormStore(realOptions).connect(this.clientSessionRepo)
return new TypeormStore(options).connect(this.clientSessionRepo)
}
async getSessionSecret(): Promise<string | undefined> {

View File

@@ -1297,7 +1297,7 @@
const durationDayjs = dayjs.duration(x.duration, 'seconds');
const durationDisplay = durationDayjs.humanize();
const cancelLink = `<a href="#" data-id="${x.id}" data-subreddit="${x.subreddit}" class="delayCancel">CANCEL</a>`;
return `<div>A <a href="https://reddit.com${x.permalink}">${x.submissionId !== undefined ? 'Comment' : 'Submission'}</a>${isAll ? ` in <a href="https://reddit.com${x.subreddit}">${x.subreddit}</a> ` : ''} by <a href="https://reddit.com/u/${x.author}">${x.author}</a> queued by ${x.source} at ${queuedAtDisplay} for ${durationDisplay} (dispatches ${durationUntilNow.humanize(true)}) -- ${cancelLink}</div>`;
return `<div>A <a href="https://reddit.com${x.permalink}">${x.submissionId !== undefined ? 'Comment' : 'Submission'}</a> by <a href="https://reddit.com/u/${x.author}">${x.author}</a>${isAll ? `, dispatched in <a href="https://reddit.com${x.subreddit}">${x.subreddit}</a> ,` : ''} queued by ${x.source} at ${queuedAtDisplay} for ${durationDisplay} (dispatches ${durationUntilNow.humanize(true)}) -- ${cancelLink}</div>`;
});
//let sub = resp.name;
if(sub === 'All') {

View File

@@ -46,14 +46,7 @@ import {ErrorWithCause, stackWithCauses} from "pony-cause";
import stringSimilarity from 'string-similarity';
import calculateCosineSimilarity from "./Utils/StringMatching/CosineSimilarity";
import levenSimilarity from "./Utils/StringMatching/levenSimilarity";
import {
isRateLimitError,
isRequestError,
isScopeError,
isSeriousError,
isStatusError,
SimpleError
} from "./Utils/Errors";
import {isRateLimitError, isRequestError, isScopeError, isStatusError, SimpleError} from "./Utils/Errors";
import merge from "deepmerge";
import {RulePremise} from "./Common/Entities/RulePremise";
import {RuleResultEntity as RuleResultEntity} from "./Common/Entities/RuleResultEntity";
@@ -77,20 +70,19 @@ import {
import {
ActivitySourceData,
ActivitySourceTypes,
ActivitySourceValue, asSubredditPlaceholder,
ActivitySourceValue,
ConfigFormat,
DurationVal,
ExternalUrlContext,
ImageHashCacheData,
ModUserNoteLabel,
modUserNoteLabels,
PollOn, pollOnTypeMapping, pollOnTypes,
RedditEntity,
RedditEntityType,
RelativeDateTimeMatch,
statFrequencies,
StatisticFrequency,
StatisticFrequencyOption, subredditPlaceholder, SubredditPlaceholderType,
StatisticFrequencyOption,
UrlContext,
WikiContext
} from "./Common/Infrastructure/Atomic";
@@ -111,7 +103,7 @@ import {
} from "./Common/Infrastructure/Filters/FilterShapes";
import {
ActivityType,
AuthorHistoryType, CMBannedUser,
AuthorHistoryType,
FullNameTypes,
PermalinkRedditThings,
RedditThing,
@@ -1103,22 +1095,16 @@ export const createRetryHandler = (opts: RetryOptions, logger: Logger) => {
// if it's a request error but not a known "oh probably just a reddit blip" status code treat it as other, which should usually have a lower retry max
}
let prefix = '';
if(isSeriousError(err)) {
// linear backoff
otherRetryCount++;
} else {
prefix = 'NON-SERIOUS ';
}
// linear backoff
otherRetryCount++;
let msg = redditApiError ? `Error occurred while making a request to Reddit (${otherRetryCount}/${maxOtherRetry} in ${clearRetryCountAfter} minutes) but it was NOT a well-known "reddit blip" error.` : `Non-request error occurred (${otherRetryCount}/${maxOtherRetry} in ${clearRetryCountAfter} minutes).`;
if (maxOtherRetry < otherRetryCount) {
logger.warn(`${prefix}${msg} Exceeded max allowed.`);
logger.warn(`${msg} Exceeded max allowed.`);
return false;
}
if(waitOnRetry) {
const ms = (4 * 1000) * otherRetryCount;
logger.warn(`${prefix}${msg} Will wait ${formatNumber(ms / 1000)} seconds before retrying`);
logger.warn(`${msg} Will wait ${formatNumber(ms / 1000)} seconds before retrying`);
await sleep(ms);
}
return true;
@@ -1570,7 +1556,7 @@ export const testMaybeStringRegex = (test: string, subject: string, defaultFlags
}
export const isStrongSubredditState = (value: SubredditCriteria | StrongSubredditCriteria) => {
return value.name === undefined || value.name instanceof RegExp || asSubredditPlaceholder(value.name);
return value.name === undefined || value.name instanceof RegExp;
}
export const asStrongSubredditState = (value: any): value is StrongSubredditCriteria => {
@@ -1588,26 +1574,21 @@ export const toStrongSubredditState = (s: SubredditCriteria, opts?: StrongSubred
let nameValOriginallyRegex = false;
let nameReg: RegExp | undefined | SubredditPlaceholderType;
let nameReg: RegExp | undefined;
if (nameValRaw !== undefined) {
if (!(nameValRaw instanceof RegExp)) {
let nameVal = nameValRaw.trim();
if(asSubredditPlaceholder(nameVal)) {
nameReg = subredditPlaceholder;
nameValOriginallyRegex = false;
nameReg = parseStringToRegex(nameVal, defaultFlags);
if (nameReg === undefined) {
// if sub state has `isUserProfile=true` and config did not provide a regex then
// assume the user wants to use the value in "name" to look for a user profile so we prefix created regex with u_
const parsedEntity = parseRedditEntity(nameVal, isUserProfile !== undefined && isUserProfile ? 'user' : 'subreddit');
// technically they could provide "u_Username" as the value for "name" and we will then match on it regardless of isUserProfile
// but like...why would they do that? There shouldn't be any subreddits that start with u_ that aren't user profiles anyway(?)
const regPrefix = parsedEntity.type === 'user' ? 'u_' : '';
nameReg = parseStringToRegex(`/^${regPrefix}${nameVal}$/`, defaultFlags);
} else {
nameReg = parseStringToRegex(nameVal, defaultFlags);
if (nameReg === undefined) {
// if sub state has `isUserProfile=true` and config did not provide a regex then
// assume the user wants to use the value in "name" to look for a user profile so we prefix created regex with u_
const parsedEntity = parseRedditEntity(nameVal, isUserProfile !== undefined && isUserProfile ? 'user' : 'subreddit');
// technically they could provide "u_Username" as the value for "name" and we will then match on it regardless of isUserProfile
// but like...why would they do that? There shouldn't be any subreddits that start with u_ that aren't user profiles anyway(?)
const regPrefix = parsedEntity.type === 'user' ? 'u_' : '';
nameReg = parseStringToRegex(`/^${regPrefix}${nameVal}$/`, defaultFlags);
} else {
nameValOriginallyRegex = true;
}
nameValOriginallyRegex = true;
}
} else {
nameValOriginallyRegex = true;
@@ -2874,7 +2855,7 @@ export const generateSnoowrapEntityFromRedditThing = (data: RedditThing, client:
case 'user':
return new RedditUser({id: data.val}, client, false);
case 'subreddit':
return new Subreddit({id: data.val}, client, false);
return new Subreddit({name: data.val}, client, false);
case 'message':
return new PrivateMessage({id: data.val}, client, false)
@@ -3107,24 +3088,3 @@ export const toStrongSharingACLConfig = (data: SharingACLConfig | string[]): Str
exclude: (data.exclude ?? []).map(x => parseStringToRegexOrLiteralSearch(x))
}
}
export const toPollOn = (val: string | PollOn): PollOn => {
const clean = val.toLowerCase().trim();
const pVal = pollOnTypeMapping.get(clean);
if(pVal !== undefined) {
return pVal;
}
throw new SimpleError(`'${val}' is not a valid polling source. Valid sources: ${pollOnTypes.join(' | ')}`);
}
export const humanizeBanDetails = (banDetails: CMBannedUser | undefined): string => {
if(banDetails === undefined) {
return 'Not Banned';
}
const timeSinceBan = dayjs.duration(dayjs().diff(banDetails.date)).humanize(true);
if(banDetails.days_left === undefined) {
return `Banned permanently ${timeSinceBan}`;
}
return `Banned ${timeSinceBan} with ${banDetails.days_left?.days()} days left`;
}