Compare commits

..

15 Commits

Author SHA1 Message Date
FoxxMD
e36325278f feat: Add banned author criteria
* Test simple true/false is banned?
* Test against ban criteria -- bannedAt date, days left, mod note(s), and permanent bool
  * Test list of ban criteria with OR condition
2023-05-17 13:11:59 -04:00
FoxxMD
8080a0b058 feat: Add banned user lookup functionality
* Retrieve author's ban status in subreddit on a per-name basis
* Cache author's ban status for AuthorTTL
2023-05-17 11:49:13 -04:00
FoxxMD
f7dc9222d6 feat(template): Add flair text props to template rendering for submissions and authors
#141
2023-04-21 10:06:52 -04:00
FoxxMD
021e5c524b feat(cache): Cache reddit request errors when fetching activities
Use small, in-memory cache to store reddit api response errors for 5 seconds to prevent wasting api calls
2023-01-30 12:22:12 -05:00
FoxxMD
e5fe4589e0 fix: Prevent non-serious errors from contributing to retry counts
Do not increase retry count if any error in the stack was marked as non-serious.
2023-01-30 12:20:43 -05:00
FoxxMD
580a9c8fe6 feat(filter): Implement own-subreddit placeholder for subreddit filtering
When subreddit criteria name is `{{subreddit}}` CM checks the given subreddit against the name of the subreddit the bot is currently operating in.
2022-12-16 10:26:42 -05:00
FoxxMD
ef372e531e fix(database): Prevent usage of LIMIT in session storage driver when db backend is mysql/mariadb
Related to freshgiammi-lab/connect-typeorm#8

Closes #128
2022-11-29 09:47:55 -05:00
FoxxMD
fde2836208 chore: remove comments about wrong endpoints
At some point maybe this was fixed by reddit silently?
2022-11-28 14:36:33 -05:00
Matt Foxx
021dd5b0c5 Merge pull request #130 from rysie/bug/selecftlair-fix 2022-11-28 14:35:39 -05:00
Marcin Macinski
5bd38d367a assignFlair doesn't work with flair_template_id 2022-11-25 17:13:49 +01:00
FoxxMD
e79779d980 feat: Implement templating for flair actions 2022-11-21 11:43:34 -05:00
FoxxMD
b094b72d4a docs: Update docker compose instructions
* Specify docker-compose minimum version
* Change commands to use new syntax
2022-11-21 11:29:14 -05:00
FoxxMD
d90e88360d feat: Add some author properties for templating 2022-11-17 13:16:10 -05:00
FoxxMD
9031f7fec8 refactor(polling): Improve resilience for polling source parsing
* Replace hard-coded polling sources with string constants
* Implement string to PollOn parsing which is case-insensitive and forgives mispelling
* Check that manager specifies only one of each polling source type when build config
2022-11-17 12:03:55 -05:00
FoxxMD
0a2b13e4c4 fix: Fix including self activities in Recent Activity without filtering
* Consolidate ACID check for author history results into authorActivities function so it can be used everywhere
* Remove self check in Recent Activity and used consolidated functionality so that filtering still occurs on current activity
2022-11-16 10:18:37 -05:00
32 changed files with 3522 additions and 607 deletions

View File

@@ -24,6 +24,9 @@ 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,6 +55,8 @@ 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.
@@ -62,13 +64,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,17 +57,42 @@ 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 |
| 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 |
### Comments

View File

@@ -67,13 +67,15 @@ export class BanAction extends Action {
// @ts-ignore
const fetchedSub = await item.subreddit.fetch();
const fetchedName = await item.author.name;
const bannedUser = await fetchedSub.banUser({
const banData = {
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,30 +30,25 @@ 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 = [];
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 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}`);
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) {
// 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(() => {});
await item.selectFlair({flair_template_id: this.flair_template_id}).then(() => {});
item.link_flair_template_id = this.flair_template_id;
} else {
await item.assignFlair({text: this.text, cssClass: this.css}).then(() => {});
item.link_flair_css_class = this.css;
item.link_flair_text = this.text;
await item.assignFlair({text: renderedText, cssClass: renderedCss}).then(() => {});
item.link_flair_css_class = renderedCss;
item.link_flair_text = renderedText;
}
await this.resources.resetCacheForItem(item);
}

View File

@@ -26,6 +26,8 @@ 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}`)
@@ -34,10 +36,12 @@ export class UserFlairAction extends Action {
}
} else {
if (this.text !== undefined) {
flairParts.push(`Text: ${this.text}`);
renderedText = await this.renderContent(this.text, item, ruleResults, actionResults) as string;
flairParts.push(`Text: ${renderedText}`);
}
if (this.css !== undefined) {
flairParts.push(`CSS: ${this.css}`);
renderedCss = await this.renderContent(this.css, item, ruleResults, actionResults) as string;
flairParts.push(`CSS: ${renderedCss}`);
}
}
@@ -58,7 +62,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 (this.text === undefined && this.css === undefined) {
} else if (renderedText === undefined && renderedCss === undefined) {
// @ts-ignore
await item.subreddit.deleteUserFlair(item.author.name);
item.author_flair_css_class = null;
@@ -68,11 +72,11 @@ export class UserFlairAction extends Action {
// @ts-ignore
await item.author.assignFlair({
subredditName: item.subreddit.display_name,
cssClass: this.css,
text: this.text,
cssClass: renderedCss,
text: renderedText,
});
item.author_flair_text = this.text ?? null;
item.author_flair_css_class = this.css ?? null;
item.author_flair_text = renderedText ?? null;
item.author_flair_css_class = renderedCss ?? null;
}
await this.resources.resetCacheForItem(item);
if(typeof item.author !== 'string') {

View File

@@ -46,7 +46,13 @@ 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, PollOn} from "../Common/Infrastructure/Atomic";
import {
Invokee,
POLLING_COMMENTS, POLLING_MODQUEUE,
POLLING_SUBMISSIONS,
POLLING_UNMODERATED,
PollOn
} from "../Common/Infrastructure/Atomic";
import {FilterCriteriaDefaults} from "../Common/Infrastructure/Filters/FilterShapes";
import {snooLogWrapper} from "../Utils/loggerFactory";
import {InfluxClient} from "../Common/Influx/InfluxClient";
@@ -558,9 +564,9 @@ class Bot implements BotInstanceFunctions {
parseSharedStreams() {
const sharedCommentsSubreddits = !this.sharedStreams.includes('newComm') ? [] : this.subManagers.filter(x => x.isPollingShared('newComm')).map(x => x.subreddit.display_name);
const sharedCommentsSubreddits = !this.sharedStreams.includes(POLLING_COMMENTS) ? [] : this.subManagers.filter(x => x.isPollingShared(POLLING_COMMENTS)).map(x => x.subreddit.display_name);
if (sharedCommentsSubreddits.length > 0) {
const stream = this.cacheManager.modStreams.get('newComm');
const stream = this.cacheManager.modStreams.get(POLLING_COMMENTS);
if (stream === undefined || stream.subreddit !== sharedCommentsSubreddits.join('+')) {
let processed;
if (stream !== undefined) {
@@ -580,20 +586,20 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultCommentStream.on('error', this.createSharedStreamErrorListener('newComm'));
defaultCommentStream.on('listing', this.createSharedStreamListingListener('newComm'));
this.cacheManager.modStreams.set('newComm', defaultCommentStream);
defaultCommentStream.on('error', this.createSharedStreamErrorListener(POLLING_COMMENTS));
defaultCommentStream.on('listing', this.createSharedStreamListingListener(POLLING_COMMENTS));
this.cacheManager.modStreams.set(POLLING_COMMENTS, defaultCommentStream);
}
} else {
const stream = this.cacheManager.modStreams.get('newComm');
const stream = this.cacheManager.modStreams.get(POLLING_COMMENTS);
if (stream !== undefined) {
stream.end('Determined no managers are listening on shared stream parsing');
}
}
const sharedSubmissionsSubreddits = !this.sharedStreams.includes('newSub') ? [] : this.subManagers.filter(x => x.isPollingShared('newSub')).map(x => x.subreddit.display_name);
const sharedSubmissionsSubreddits = !this.sharedStreams.includes(POLLING_SUBMISSIONS) ? [] : this.subManagers.filter(x => x.isPollingShared(POLLING_SUBMISSIONS)).map(x => x.subreddit.display_name);
if (sharedSubmissionsSubreddits.length > 0) {
const stream = this.cacheManager.modStreams.get('newSub');
const stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS);
if (stream === undefined || stream.subreddit !== sharedSubmissionsSubreddits.join('+')) {
let processed;
if (stream !== undefined) {
@@ -613,19 +619,19 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultSubStream.on('error', this.createSharedStreamErrorListener('newSub'));
defaultSubStream.on('listing', this.createSharedStreamListingListener('newSub'));
this.cacheManager.modStreams.set('newSub', defaultSubStream);
defaultSubStream.on('error', this.createSharedStreamErrorListener(POLLING_SUBMISSIONS));
defaultSubStream.on('listing', this.createSharedStreamListingListener(POLLING_SUBMISSIONS));
this.cacheManager.modStreams.set(POLLING_SUBMISSIONS, defaultSubStream);
}
} else {
const stream = this.cacheManager.modStreams.get('newSub');
const stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS);
if (stream !== undefined) {
stream.end('Determined no managers are listening on shared stream parsing');
}
}
const isUnmoderatedShared = !this.sharedStreams.includes('unmoderated') ? false : this.subManagers.some(x => x.isPollingShared('unmoderated'));
const unmoderatedstream = this.cacheManager.modStreams.get('unmoderated');
const isUnmoderatedShared = !this.sharedStreams.includes(POLLING_UNMODERATED) ? false : this.subManagers.some(x => x.isPollingShared(POLLING_UNMODERATED));
const unmoderatedstream = this.cacheManager.modStreams.get(POLLING_UNMODERATED);
if (isUnmoderatedShared && unmoderatedstream === undefined) {
const defaultUnmoderatedStream = new UnmoderatedStream(this.client, {
subreddit: 'mod',
@@ -634,15 +640,15 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultUnmoderatedStream.on('error', this.createSharedStreamErrorListener('unmoderated'));
defaultUnmoderatedStream.on('listing', this.createSharedStreamListingListener('unmoderated'));
this.cacheManager.modStreams.set('unmoderated', defaultUnmoderatedStream);
defaultUnmoderatedStream.on('error', this.createSharedStreamErrorListener(POLLING_UNMODERATED));
defaultUnmoderatedStream.on('listing', this.createSharedStreamListingListener(POLLING_UNMODERATED));
this.cacheManager.modStreams.set(POLLING_UNMODERATED, defaultUnmoderatedStream);
} else if (!isUnmoderatedShared && unmoderatedstream !== undefined) {
unmoderatedstream.end('Determined no managers are listening on shared stream parsing');
}
const isModqueueShared = !this.sharedStreams.includes('modqueue') ? false : this.subManagers.some(x => x.isPollingShared('modqueue'));
const modqueuestream = this.cacheManager.modStreams.get('modqueue');
const isModqueueShared = !this.sharedStreams.includes(POLLING_MODQUEUE) ? false : this.subManagers.some(x => x.isPollingShared(POLLING_MODQUEUE));
const modqueuestream = this.cacheManager.modStreams.get(POLLING_MODQUEUE);
if (isModqueueShared && modqueuestream === undefined) {
const defaultModqueueStream = new ModQueueStream(this.client, {
subreddit: 'mod',
@@ -651,9 +657,9 @@ class Bot implements BotInstanceFunctions {
label: 'Shared Polling'
});
// @ts-ignore
defaultModqueueStream.on('error', this.createSharedStreamErrorListener('modqueue'));
defaultModqueueStream.on('listing', this.createSharedStreamListingListener('modqueue'));
this.cacheManager.modStreams.set('modqueue', defaultModqueueStream);
defaultModqueueStream.on('error', this.createSharedStreamErrorListener(POLLING_MODQUEUE));
defaultModqueueStream.on('listing', this.createSharedStreamListingListener(POLLING_MODQUEUE));
this.cacheManager.modStreams.set(POLLING_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, DataSource, JoinColumn} from "typeorm";
import {Entity, Column, ManyToOne, PrimaryColumn, OneToMany, Index} from "typeorm";
import {AuthorEntity} from "./AuthorEntity";
import {Subreddit} from "./Subreddit";
import {CMEvent} from "./CMEvent";
@@ -6,8 +6,6 @@ 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
@@ -47,7 +45,7 @@ export class Activity {
@Column({name: 'name'})
name!: string;
@ManyToOne(type => Subreddit, sub => sub.activities, {cascade: ['insert'], eager: true})
@ManyToOne(type => Subreddit, sub => sub.activities, {cascade: ['insert']})
subreddit!: Subreddit;
@Column("varchar", {length: 20})
@@ -60,18 +58,17 @@ export class Activity {
@Column("text")
permalink!: string;
@ManyToOne(type => AuthorEntity, author => author.activities, {cascade: ['insert'], eager: true})
@ManyToOne(type => AuthorEntity, author => author.activities, {cascade: ['insert']})
author!: AuthorEntity;
@OneToMany(type => CMEvent, act => act.activity)
@OneToMany(type => CMEvent, act => act.activity) // note: we will create author property in the Photo class below
actionedEvents!: CMEvent[]
@ManyToOne('Activity', 'comments', {nullable: true, cascade: ['insert']})
@JoinColumn({name: 'submission_id'})
@ManyToOne(type => Activity, obj => obj.comments, {nullable: true})
submission?: Activity;
@OneToMany('Activity', 'submission', {nullable: true})
comments?: Activity[];
@OneToMany(type => Activity, obj => obj.submission, {nullable: true})
comments!: Activity[];
@OneToMany(type => ActivityReport, act => act.activity, {cascade: ['insert'], eager: true})
reports: ActivityReport[] | undefined
@@ -154,12 +151,10 @@ export class Activity {
return false;
}
static async fromSnoowrapActivity(activity: SnoowrapActivity, options: fromSnoowrapOptions | undefined = {}) {
static fromSnoowrapActivity(subreddit: Subreddit, activity: SnoowrapActivity, lastKnownStateTimestamp?: dayjs.Dayjs | 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;
@@ -184,30 +179,8 @@ export class Activity {
submission
});
entity.syncReports(activity, options.lastKnownStateTimestamp);
entity.syncReports(activity, 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,8 +1,5 @@
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 {
@@ -14,15 +11,11 @@ export class AuthorEntity {
name!: string;
@OneToMany(type => Activity, act => act.author)
activities!: Promise<Activity[]>
activities!: 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, JoinColumn
AfterLoad
} 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 {
//@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()
activityId!: string
@Column()
author!: string
@Column({
type: 'int',
@@ -82,10 +82,11 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
}})
tardyTolerant!: boolean | Duration
constructor(data?: HydratedActivityDispatch) {
constructor(data?: ActivityDispatch & { manager: ManagerEntity }) {
super();
if (data !== undefined) {
this.activity = data.activity;
this.activityId = data.activity.name;
this.author = getActivityAuthorName(data.activity.author);
this.delay = data.delay;
this.createdAt = data.queuedAt;
this.type = data.type;
@@ -150,7 +151,20 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
}
async toActivityDispatch(client: ExtendedSnoowrap): Promise<ActivityDispatch> {
let activity = this.activity.toSnoowrap(client);
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;
return {
id: this.id,
queuedAt: this.createdAt,
@@ -162,13 +176,8 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
cancelIfQueued: this.cancelIfQueued,
identifier: this.identifier,
type: this.type,
author: activity.author.name,
author: this.author,
dryRun: this.dryRun
}
}
}
export interface HydratedActivityDispatch extends Omit<ActivityDispatch, 'activity'> {
activity: Activity
manager: ManagerEntity
}

View File

@@ -1,7 +1,5 @@
import {Entity, Column, PrimaryColumn, OneToMany, Index, DataSource} from "typeorm";
import {Entity, Column, PrimaryColumn, OneToMany, Index} from "typeorm";
import {Activity} from "./Activity";
import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients";
import {Subreddit as SnoowrapSubreddit} from "snoowrap/dist/objects";
export interface SubredditEntityOptions {
id: string
@@ -27,18 +25,4 @@ 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,6 +111,19 @@ 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';
@@ -395,3 +408,9 @@ 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
ModUserNoteLabel, RelativeDateTimeMatch, SubredditPlaceholderType
} 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
name?: RegExp | SubredditPlaceholderType
}
export const defaultStrongSubredditCriteriaOptions = {
@@ -289,7 +289,57 @@ export const toFullModLogCriteria = (val: ModLogCriteria): FullModLogCriteria =>
}, {});
}
export const authorCriteriaProperties = ['name', 'flairCssClass', 'flairText', 'flairTemplate', 'isMod', 'userNotes', 'modActions', 'age', 'linkKarma', 'commentKarma', 'totalKarma', 'verified', 'shadowBanned', 'description', 'isContributor'];
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
}
/**
* Criteria with which to test against the author of an Activity. The outcome of the test is based on:
@@ -428,6 +478,19 @@ 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[]
}
/**
@@ -455,6 +518,7 @@ 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,6 +1,9 @@
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;
@@ -166,3 +169,16 @@ 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

@@ -1,19 +0,0 @@
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: 'unmoderated' | 'modqueue' | 'newSub' | 'newComm'
pollOn: PollOn
}
export interface TTLConfig {

View File

@@ -8,7 +8,7 @@ import {
overwriteMerge,
parseBool, parseExternalUrl, parseUrlContext, parseWikiContext, randomId,
readConfigFile,
removeUndefinedKeys, resolvePathFromEnvWithRelative, toStrongSharingACLConfig
removeUndefinedKeys, resolvePathFromEnvWithRelative, toPollOn, 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,
PollOn
CacheProvider, ConfigFormat, ConfigFragmentParseFunc, POLLING_MODQUEUE, POLLING_UNMODERATED,
PollOn, pollOnTypes
} from "./Common/Infrastructure/Atomic";
import {
asFilterOptionsJson,
@@ -452,27 +452,31 @@ 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') {
opts.push({
pollOn: v as PollOn,
interval: DEFAULT_POLLING_INTERVAL,
limit: DEFAULT_POLLING_LIMIT,
});
rawOpts = {pollOn: v as PollOn}; // maybeee
} else {
const {
pollOn: p,
interval = DEFAULT_POLLING_INTERVAL,
limit = DEFAULT_POLLING_LIMIT,
delayUntil,
} = v;
opts.push({
pollOn: p as PollOn,
interval,
limit,
delayUntil,
});
rawOpts = v;
}
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;
}
@@ -796,7 +800,7 @@ export const parseDefaultBotInstanceFromArgs = (args: any): BotInstanceJsonConfi
heartbeatInterval: heartbeat,
},
polling: {
shared: sharedMod ? ['unmoderated', 'modqueue'] : undefined,
shared: sharedMod ? [POLLING_UNMODERATED, POLLING_MODQUEUE] : undefined,
},
nanny: {
softLimit,
@@ -908,7 +912,7 @@ export const parseDefaultBotInstanceFromEnv = (): BotInstanceJsonConfig => {
heartbeatInterval: process.env.HEARTBEAT !== undefined ? parseInt(process.env.HEARTBEAT) : undefined,
},
polling: {
shared: parseBool(process.env.SHARE_MOD) ? ['unmoderated', 'modqueue'] : undefined,
shared: parseBool(process.env.SHARE_MOD) ? [POLLING_UNMODERATED, POLLING_MODQUEUE] : undefined,
},
nanny: {
softLimit: process.env.SOFT_LIMIT !== undefined ? parseInt(process.env.SOFT_LIMIT) : undefined,
@@ -1525,10 +1529,10 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
botCache.provider.prefix = buildCachePrefix([botCache.provider.prefix, 'bot', (botName || objectHash.sha1(botCreds))]);
}
let realShared = shared === true ? ['unmoderated', 'modqueue', 'newComm', 'newSub'] : shared;
let realShared: PollOn[] = shared === true ? pollOnTypes : shared.map(toPollOn);
if (sharedMod === true) {
realShared.push('unmoderated');
realShared.push('modqueue');
realShared.push(POLLING_UNMODERATED);
realShared.push(POLLING_MODQUEUE);
}
const botLevelStatDefaults = {...statDefaultsFromOp, ...databaseStatisticsDefaults};
@@ -1566,7 +1570,7 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo
caching: botCache,
userAgent,
polling: {
shared: [...new Set(realShared)] as PollOn[],
shared: Array.from(new Set(realShared)),
stagger,
limit,
interval,

View File

@@ -126,28 +126,7 @@ 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) {
@@ -159,25 +138,10 @@ export class RecentActivityRule extends Rule {
}
}
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;
}
// 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]);
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,6 +59,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -77,6 +83,9 @@
},
{
"$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."
@@ -100,6 +109,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -121,6 +139,9 @@
},
{
"$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}]]"
@@ -180,6 +201,23 @@
"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*(%?)(.*)$",
@@ -345,6 +383,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -363,6 +407,9 @@
},
{
"$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."
@@ -409,6 +456,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -430,6 +486,9 @@
},
{
"$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}]]"
@@ -478,11 +537,50 @@
],
"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": [
@@ -501,6 +599,9 @@
},
{
"$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."
@@ -540,6 +641,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -561,6 +671,9 @@
},
{
"$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}]]"
@@ -613,8 +726,18 @@
"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": [
@@ -633,6 +756,9 @@
},
{
"$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."
@@ -678,6 +804,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -699,6 +834,9 @@
},
{
"$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}]]"
@@ -969,6 +1107,12 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -987,6 +1131,9 @@
},
{
"$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."
@@ -1010,6 +1157,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1031,6 +1187,9 @@
},
{
"$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}]]"
@@ -1069,6 +1228,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1087,6 +1252,9 @@
},
{
"$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."
@@ -1172,6 +1340,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1193,6 +1370,9 @@
},
{
"$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}]]"
@@ -1425,6 +1605,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1443,6 +1629,9 @@
},
{
"$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."
@@ -1474,6 +1663,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1495,6 +1693,9 @@
},
{
"$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}]]"
@@ -1564,6 +1765,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1582,6 +1789,9 @@
},
{
"$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."
@@ -1605,6 +1815,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1626,6 +1845,9 @@
},
{
"$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}]]"
@@ -1660,6 +1882,12 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1678,6 +1906,9 @@
},
{
"$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."
@@ -1719,6 +1950,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1740,6 +1980,9 @@
},
{
"$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}]]"
@@ -1903,6 +2146,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1921,6 +2170,9 @@
},
{
"$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."
@@ -1968,6 +2220,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1989,6 +2250,9 @@
},
{
"$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}]]"
@@ -2222,6 +2486,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2240,6 +2510,9 @@
},
{
"$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."
@@ -2263,6 +2536,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2284,6 +2566,9 @@
},
{
"$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}]]"
@@ -2326,6 +2611,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2344,6 +2635,9 @@
},
{
"$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."
@@ -2376,6 +2670,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2397,6 +2700,9 @@
},
{
"$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}]]"
@@ -2427,6 +2733,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2445,6 +2757,9 @@
},
{
"$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."
@@ -2503,6 +2818,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2524,6 +2848,9 @@
},
{
"$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}]]"
@@ -2885,6 +3212,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2903,6 +3236,9 @@
},
{
"$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."
@@ -2934,6 +3270,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2955,6 +3300,9 @@
},
{
"$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}]]"
@@ -2997,6 +3345,12 @@
},
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3015,6 +3369,9 @@
},
{
"$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."
@@ -3062,6 +3419,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3083,6 +3449,9 @@
},
{
"$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,6 +24,23 @@
"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*(%?)(.*)$",
@@ -184,6 +201,39 @@
},
"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/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -461,6 +461,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -479,6 +485,9 @@
},
{
"$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."
@@ -501,6 +510,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -522,6 +540,9 @@
},
{
"$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}]]"
@@ -570,6 +591,23 @@
"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*(%?)(.*)$",
@@ -734,6 +772,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -752,6 +796,9 @@
},
{
"$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."
@@ -786,6 +833,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -807,6 +863,9 @@
},
{
"$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}]]"
@@ -832,6 +891,39 @@
],
"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": [
@@ -1526,10 +1618,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1580,10 +1672,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1630,10 +1722,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1667,10 +1759,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1720,6 +1812,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1738,6 +1836,9 @@
},
{
"$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."
@@ -1808,6 +1909,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1829,6 +1939,9 @@
},
{
"$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}]]"
@@ -1999,6 +2112,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2017,6 +2136,9 @@
},
{
"$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."
@@ -2031,6 +2153,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2052,6 +2183,9 @@
},
{
"$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}]]"
@@ -2557,6 +2691,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2575,6 +2715,9 @@
},
{
"$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."
@@ -2585,6 +2728,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2606,6 +2758,9 @@
},
{
"$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}]]"
@@ -2657,10 +2812,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -2802,10 +2957,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -2830,6 +2985,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2848,6 +3009,9 @@
},
{
"$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."
@@ -2877,6 +3041,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2898,6 +3071,9 @@
},
{
"$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}]]"
@@ -2932,6 +3108,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2950,6 +3132,9 @@
},
{
"$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."
@@ -3013,6 +3198,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3034,6 +3228,9 @@
},
{
"$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}]]"
@@ -3097,10 +3294,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3232,10 +3429,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3257,6 +3454,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3275,6 +3478,9 @@
},
{
"$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."
@@ -3304,6 +3510,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3325,6 +3540,9 @@
},
{
"$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}]]"
@@ -3435,10 +3653,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3463,6 +3681,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3481,6 +3705,9 @@
},
{
"$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."
@@ -3509,6 +3736,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3530,6 +3766,9 @@
},
{
"$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/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -426,6 +426,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -444,6 +450,9 @@
},
{
"$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."
@@ -466,6 +475,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -487,6 +505,9 @@
},
{
"$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}]]"
@@ -535,6 +556,23 @@
"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*(%?)(.*)$",
@@ -699,6 +737,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -717,6 +761,9 @@
},
{
"$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."
@@ -751,6 +798,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -772,6 +828,9 @@
},
{
"$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}]]"
@@ -797,6 +856,39 @@
],
"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": [
@@ -1491,10 +1583,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1545,10 +1637,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1595,10 +1687,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1632,10 +1724,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -1685,6 +1777,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1703,6 +1801,9 @@
},
{
"$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."
@@ -1773,6 +1874,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -1794,6 +1904,9 @@
},
{
"$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}]]"
@@ -1964,6 +2077,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -1982,6 +2101,9 @@
},
{
"$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."
@@ -1996,6 +2118,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2017,6 +2148,9 @@
},
{
"$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}]]"
@@ -2522,6 +2656,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2540,6 +2680,9 @@
},
{
"$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."
@@ -2550,6 +2693,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2571,6 +2723,9 @@
},
{
"$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}]]"
@@ -2622,10 +2777,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -2767,10 +2922,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -2795,6 +2950,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2813,6 +2974,9 @@
},
{
"$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."
@@ -2842,6 +3006,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2863,6 +3036,9 @@
},
{
"$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}]]"
@@ -2897,6 +3073,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -2915,6 +3097,9 @@
},
{
"$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."
@@ -2978,6 +3163,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -2999,6 +3193,9 @@
},
{
"$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}]]"
@@ -3062,10 +3259,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3197,10 +3394,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3222,6 +3419,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3240,6 +3443,9 @@
},
{
"$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."
@@ -3269,6 +3475,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3290,6 +3505,9 @@
},
{
"$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}]]"
@@ -3400,10 +3618,10 @@
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
"$ref": "#/definitions/FullActivityWindowConfig"
},
{
"$ref": "#/definitions/FullActivityWindowConfig"
"$ref": "#/definitions/DurationObject"
},
{
"type": [
@@ -3428,6 +3646,12 @@
"properties": {
"authorIs": {
"anyOf": [
{
"$ref": "#/definitions/AuthorCriteria"
},
{
"$ref": "#/definitions/NamedCriteria<AuthorCriteria>"
},
{
"items": {
"anyOf": [
@@ -3446,6 +3670,9 @@
},
{
"$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."
@@ -3474,6 +3701,15 @@
},
"itemIs": {
"anyOf": [
{
"$ref": "#/definitions/SubmissionState"
},
{
"$ref": "#/definitions/CommentState"
},
{
"$ref": "#/definitions/NamedCriteria<TypedActivityState>"
},
{
"items": {
"anyOf": [
@@ -3495,6 +3731,9 @@
},
{
"$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,
PollOn,
Invokee, POLLING_COMMENTS, POLLING_MODQUEUE, POLLING_SUBMISSIONS, POLLING_UNMODERATED,
PollOn, pollOnTypes,
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: 'unmoderated', limit: DEFAULT_POLLING_LIMIT, interval: DEFAULT_POLLING_INTERVAL}],
polling = [{pollOn: POLLING_SUBMISSIONS, 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 === 'modqueue') {
if(options.activitySource.identifier === POLLING_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 = await Activity.fromSnoowrapActivity(activity, {lastKnownStateTimestamp, db: this.resources.database});
activityEntity = Activity.fromSnoowrapActivity(this.managerEntity.subreddit, activity, lastKnownStateTimestamp);
// 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(await Activity.fromSnoowrapActivity(sub, {db: this.resources.database}));
subActivity = await this.activityRepo.save(Activity.fromSnoowrapActivity(this.managerEntity.subreddit, sub));
}
event.activity.submission = subActivity;
@@ -1325,25 +1325,20 @@ export class Manager extends EventEmitter implements RunningStates {
}
}
isPollingShared(streamName: string): boolean {
isPollingShared(streamName: PollOn): 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 as PollOn);
return pollOption !== undefined && pollOption.limit === DEFAULT_POLLING_LIMIT && pollOption.interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(streamName);
}
async buildPolling() {
const sources: PollOn[] = ['unmoderated', 'modqueue', 'newComm', 'newSub'];
const sources = [...pollOnTypes];
const subName = this.subreddit.display_name;
for (const source of sources) {
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());
const pollOpt = this.pollOptions.find(x => x.pollOn === source);
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`);
@@ -1366,11 +1361,11 @@ export class Manager extends EventEmitter implements RunningStates {
let modStreamType: string | undefined;
switch (source) {
case 'unmoderated':
case POLLING_UNMODERATED:
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = 'unmoderated';
modStreamType = POLLING_UNMODERATED;
// use default mod stream from resources
stream = this.cacheManager.modStreams.get('unmoderated') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get(POLLING_UNMODERATED) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new UnmoderatedStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1380,11 +1375,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case 'modqueue':
case POLLING_MODQUEUE:
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = 'modqueue';
modStreamType = POLLING_MODQUEUE;
// use default mod stream from resources
stream = this.cacheManager.modStreams.get('modqueue') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get(POLLING_MODQUEUE) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new ModQueueStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1394,11 +1389,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case 'newSub':
case POLLING_SUBMISSIONS:
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = 'newSub';
modStreamType = POLLING_SUBMISSIONS;
// use default mod stream from resources
stream = this.cacheManager.modStreams.get('newSub') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get(POLLING_SUBMISSIONS) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new SubmissionStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1408,11 +1403,11 @@ export class Manager extends EventEmitter implements RunningStates {
});
}
break;
case 'newComm':
case POLLING_COMMENTS:
if (limit === DEFAULT_POLLING_LIMIT && interval === DEFAULT_POLLING_INTERVAL && this.sharedStreams.includes(source)) {
modStreamType = 'newComm';
modStreamType = POLLING_COMMENTS;
// use default mod stream from resources
stream = this.cacheManager.modStreams.get('newComm') as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
stream = this.cacheManager.modStreams.get(POLLING_COMMENTS) as SPoll<Snoowrap.Submission | Snoowrap.Comment>;
} else {
stream = new CommentStream(this.client, {
subreddit: this.subreddit.display_name,
@@ -1422,6 +1417,8 @@ 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) {
@@ -1514,10 +1511,10 @@ export class Manager extends EventEmitter implements RunningStates {
}
noChecksWarning = (source: PollOn) => (listing: any) => {
if (this.commentChecks.length === 0 && ['modqueue', 'newComm'].some(x => x === source)) {
if (this.commentChecks.length === 0 && [POLLING_MODQUEUE, POLLING_COMMENTS].some(x => x === source)) {
this.logger.warn(`Polling '${source.toUpperCase()}' may return Comments but no comments checks were configured.`);
}
if (this.submissionChecks.length === 0 && ['unmoderated', 'modqueue', 'newSub'].some(x => x === source)) {
if (this.submissionChecks.length === 0 && [POLLING_UNMODERATED, POLLING_MODQUEUE, POLLING_SUBMISSIONS].some(x => x === source)) {
this.logger.warn(`Polling '${source.toUpperCase()}' may return Submissions but no submission checks were configured.`);
}
}
@@ -1670,7 +1667,7 @@ export class Manager extends EventEmitter implements RunningStates {
}
this.startedAt = dayjs();
const modQueuePollOpts = this.pollOptions.find(x => x.pollOn === 'modqueue');
const modQueuePollOpts = this.pollOptions.find(x => x.pollOn === POLLING_MODQUEUE);
if(modQueuePollOpts !== undefined) {
this.modqueueInterval = modQueuePollOpts.interval;
}

View File

@@ -29,7 +29,7 @@ import {
generateFullWikiUrl,
generateItemFilterHelpers,
getActivityAuthorName,
getActivitySubredditName,
getActivitySubredditName, humanizeBanDetails,
isComment,
isCommentState,
isRuleSetResult,
@@ -56,7 +56,7 @@ import {
} from "../util";
import {
ActivityDispatch,
CacheConfig,
CacheConfig, CacheOptions,
Footer,
HistoricalStatsDisplay,
ResourceStats, StrongTTLConfig,
@@ -69,16 +69,7 @@ import {cacheTTLDefaults, createHistoricalDisplayDefaults,} from "../Common/defa
import {ExtendedSnoowrap} from "../Utils/SnoowrapClients";
import dayjs, {Dayjs} from "dayjs";
import ImageData from "../Common/ImageData";
import {
Between, Brackets,
DataSource,
DeleteQueryBuilder,
In,
LessThan,
NotBrackets,
Repository,
SelectQueryBuilder
} from "typeorm";
import {Between, DataSource, DeleteQueryBuilder, LessThan, Repository, SelectQueryBuilder} from "typeorm";
import {CMEvent as ActionedEventEntity, CMEvent} from "../Common/Entities/CMEvent";
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
import globrex from 'globrex';
@@ -97,7 +88,7 @@ import cloneDeep from "lodash/cloneDeep";
import {
asModLogCriteria,
asModNoteCriteria,
AuthorCriteria,
AuthorCriteria, BanCriteria,
cmToSnoowrapActivityMap, cmToSnoowrapAuthorMap,
CommentState,
ModLogCriteria,
@@ -113,7 +104,7 @@ import {
UserNoteCriteria
} from "../Common/Infrastructure/Filters/FilterCriteria";
import {
ActivitySourceValue,
ActivitySourceValue, asSubredditPlaceholder,
ConfigFragmentParseFunc,
DurationVal,
EventRetentionPolicyRange,
@@ -124,7 +115,7 @@ import {
ModUserNoteLabel,
RelativeDateTimeMatch,
statFrequencies,
StatisticFrequencyOption,
StatisticFrequencyOption, SubredditPlaceholderType,
WikiContext
} from "../Common/Infrastructure/Atomic";
import {
@@ -145,9 +136,9 @@ import {Duration} from "dayjs/plugin/duration";
import {
ActivityType,
AuthorHistorySort,
CachedFetchedActivitiesResult,
CachedFetchedActivitiesResult, CMBannedUser,
FetchedActivitiesResult, MaybeActivityType, RedditUserLike,
SnoowrapActivity,
SnoowrapActivity, SnoowrapBannedUser,
SubredditLike,
SubredditRemovalReason
} from "../Common/Infrastructure/Reddit";
@@ -170,9 +161,9 @@ import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
import {ActivitySource} from "../Common/ActivitySource";
import {SubredditResourceOptions} from "../Common/Subreddit/SubredditResourceInterfaces";
import {SubredditStats} from "./Stats";
import {CMCache} from "../Common/Cache";
import { Activity } from '../Common/Entities/Activity';
import {FindOptionsWhere} from "typeorm/find-options/FindOptionsWhere";
import {CMCache, createCacheManager} from "../Common/Cache";
import {BannedUser, BanOptions} from "snoowrap/dist/objects/Subreddit";
import {testBanCriteria} from "../Utils/Criteria/AuthorCritUtils";
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.';
@@ -198,13 +189,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
@@ -241,7 +232,6 @@ 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;
@@ -257,6 +247,12 @@ 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);
@@ -417,25 +413,21 @@ export class SubredditResources {
}
},
relations: {
manager: true,
activity: {
submission: true
}
manager: 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.activity.id} queued at ${dAct.createdAt.format('YYYY-MM-DD HH:mm:ssZ')} for ${dAct.delay.humanize()} is now LATE`;
let tardyHint = `Activity ${dAct.activityId} 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);
toRemove.push(dAct.id);
await this.removeDelayedActivity(dAct.id);
continue;
} else {
// see if its within tolerance
@@ -443,7 +435,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);
toRemove.push(dAct.id);
await this.removeDelayedActivity(dAct.id);
continue;
} else {
tardyHint += `but is within tardy tolerance of ${dAct.tardyTolerant.humanize()} of planned dispatch time so will be dispatched immediately`;
@@ -456,115 +448,27 @@ export class SubredditResources {
try {
this.delayedItems.push(await dAct.toActivityDispatch(this.client))
} catch (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}));
this.logger.warn(new ErrorWithCause(`Unable to add Activity ${dAct.activityId} from database delayed activities to in-app delayed activities queue`, {cause: e}));
}
}
if(toRemove.length > 0) {
await this.removeDelayedActivity(toRemove);
}
}
}
async addDelayedActivity(data: ActivityDispatch) {
// 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})}));
const dEntity = await this.dispatchedActivityRepo.save(new DispatchedEntity({...data, manager: this.managerEntity}));
data.id = dEntity.id;
this.delayedItems.push(data);
}
async removeDelayedActivity(val?: string | string[]) {
let dispatched: DispatchedEntity[] = [];
const where: FindOptionsWhere<DispatchedEntity> = {
manager: {
id: this.managerEntity.id
}
};
if(val !== undefined) {
const ids = typeof val === 'string' ? [val] : val;
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);
if(val === undefined) {
await this.dispatchedActivityRepo.delete({manager: {id: this.managerEntity.id}});
this.delayedItems = [];
} else {
this.logger.warn('No dispatched ids found to delete');
const ids = typeof val === 'string' ? [val] : val;
await this.dispatchedActivityRepo.delete(ids);
this.delayedItems = this.delayedItems.filter(x => !ids.includes(x.id));
}
if(activityIdsToDelete.length > 0) {
await this.activityRepo.delete(activityIdsToDelete);
}
this.delayedItems = this.delayedItems.filter(x => !actualDispatchedIds.includes(x.id));
}
async initStats() {
@@ -966,6 +870,47 @@ 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}`;
@@ -1135,13 +1080,13 @@ export class SubredditResources {
}
}
async getAuthorActivities(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing): Promise<SnoowrapActivity[]> {
async getAuthorActivities(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing, prefetchedActivities?: SnoowrapActivity[]): Promise<SnoowrapActivity[]> {
const {post} = await this.getAuthorActivitiesWithFilter(user, options, customListing);
const {post} = await this.getAuthorActivitiesWithFilter(user, options, customListing, prefetchedActivities);
return post;
}
async getAuthorActivitiesWithFilter(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing): Promise<FetchedActivitiesResult> {
async getAuthorActivitiesWithFilter(user: RedditUser, options: ActivityWindowCriteria, customListing?: NamedListing, prefetchedActivities?: SnoowrapActivity[]): Promise<FetchedActivitiesResult> {
let listFuncName: string;
let listFunc: ListingFunc;
@@ -1169,21 +1114,24 @@ export class SubredditResources {
...(cloneDeep(options)),
}
return await this.getActivities(user, criteriaWithDefaults, {func: listFunc, name: listFuncName});
return await this.getActivities(user, criteriaWithDefaults, {func: listFunc, name: listFuncName}, prefetchedActivities);
}
async getAuthorComments(user: RedditUser, options: ActivityWindowCriteria): Promise<Comment[]> {
return await this.getAuthorActivities(user, {...options, fetch: 'comment'}) as unknown as Promise<Comment[]>;
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 getAuthorSubmissions(user: RedditUser, options: ActivityWindowCriteria): Promise<Submission[]> {
async getAuthorSubmissions(user: RedditUser, options: ActivityWindowCriteria, prefetchedActivities?: SnoowrapActivity[]): Promise<Submission[]> {
return await this.getAuthorActivities(user, {
...options,
fetch: 'submission'
}) as unknown as Promise<Submission[]>;
}, undefined,prefetchedActivities) as unknown as Promise<Submission[]>;
}
async getActivities(user: RedditUser, options: ActivityWindowCriteria, listingData: NamedListing): Promise<FetchedActivitiesResult> {
async getActivities(user: RedditUser, options: ActivityWindowCriteria, listingData: NamedListing, prefetchedActivities: SnoowrapActivity[] = []): Promise<FetchedActivitiesResult> {
let cacheKey: string | undefined;
let fromCache = false;
try {
@@ -1192,7 +1140,6 @@ export class SubredditResources {
let apiCount = 1;
let preMaxTrigger: undefined | string;
let rawCount: number = 0;
let fromCache = false;
const hashObj = cloneDeep(options);
@@ -1205,13 +1152,23 @@ export class SubredditResources {
const userName = getActivityAuthorName(user);
const hash = objectHash.sha1(hashObj);
const cacheKey = `${userName}-${listingData.name}-${hash}`;
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) {
@@ -1318,12 +1275,24 @@ export class SubredditResources {
}
}
let unFilteredItems: SnoowrapActivity[] | undefined;
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);
const { func: listingFunc } = listingData;
let listing = await listingFunc(getAuthorHistoryAPIOptions(options));
let hitEnd = false;
let offset = chunkSize;
@@ -1333,6 +1302,9 @@ 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
@@ -1433,9 +1405,14 @@ export class SubredditResources {
} catch (err: any) {
if(isStatusError(err)) {
switch(err.statusCode) {
case 404:
throw new SimpleError('Reddit returned a 404 for user history. Likely this user is shadowbanned.', {isSerious: false});
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 MaybeSeriousErrorWithCause('Reddit returned a 403 for user history, likely this user is suspended.', {cause: err, isSerious: false});
default:
throw err;
@@ -1607,6 +1584,7 @@ export class SubredditResources {
usernotes,
ruleResults,
actionResults,
author: (val) => this.getAuthor(val)
});
}
@@ -1980,8 +1958,14 @@ export class SubredditResources {
if (crit[k] !== undefined) {
switch (k) {
case 'name':
const nameReg = crit[k] as RegExp;
if(!nameReg.test(subreddit.display_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)) {
return false;
}
break;
@@ -2804,6 +2788,33 @@ 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

@@ -0,0 +1,79 @@
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,83 +46,6 @@ 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);
@@ -133,6 +56,7 @@ export interface TemplateContext {
ruleResults?: RuleResultEntity[]
actionResults?: ActionResultEntity[]
activity?: SnoowrapActivity
author?: (val: string | RedditUser) => Promise<RedditUser>
[key: string]: any
}
@@ -140,11 +64,25 @@ 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
@@ -162,6 +100,7 @@ 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;
}
@@ -171,10 +110,25 @@ 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',
// @ts-ignore
author: getActivityAuthorName(await activity.author),
author,
votes: activity.score,
age: dayjs.duration(dayjs().diff(dayjs.unix(activity.created))).humanize(),
permalink,

View File

@@ -12,6 +12,7 @@ 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
@@ -103,7 +104,12 @@ export class DatabaseStorageProvider extends StorageProvider {
}
createSessionStore(options?: TypeormStoreOptions): Store {
return new TypeormStore(options).connect(this.clientSessionRepo)
// 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)
}
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> 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>`;
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>`;
});
//let sub = resp.name;
if(sub === 'All') {

View File

@@ -46,7 +46,14 @@ 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, isStatusError, SimpleError} from "./Utils/Errors";
import {
isRateLimitError,
isRequestError,
isScopeError,
isSeriousError,
isStatusError,
SimpleError
} from "./Utils/Errors";
import merge from "deepmerge";
import {RulePremise} from "./Common/Entities/RulePremise";
import {RuleResultEntity as RuleResultEntity} from "./Common/Entities/RuleResultEntity";
@@ -70,19 +77,20 @@ import {
import {
ActivitySourceData,
ActivitySourceTypes,
ActivitySourceValue,
ActivitySourceValue, asSubredditPlaceholder,
ConfigFormat,
DurationVal,
ExternalUrlContext,
ImageHashCacheData,
ModUserNoteLabel,
modUserNoteLabels,
PollOn, pollOnTypeMapping, pollOnTypes,
RedditEntity,
RedditEntityType,
RelativeDateTimeMatch,
statFrequencies,
StatisticFrequency,
StatisticFrequencyOption,
StatisticFrequencyOption, subredditPlaceholder, SubredditPlaceholderType,
UrlContext,
WikiContext
} from "./Common/Infrastructure/Atomic";
@@ -103,7 +111,7 @@ import {
} from "./Common/Infrastructure/Filters/FilterShapes";
import {
ActivityType,
AuthorHistoryType,
AuthorHistoryType, CMBannedUser,
FullNameTypes,
PermalinkRedditThings,
RedditThing,
@@ -1095,16 +1103,22 @@ 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
}
// linear backoff
otherRetryCount++;
let prefix = '';
if(isSeriousError(err)) {
// linear backoff
otherRetryCount++;
} else {
prefix = 'NON-SERIOUS ';
}
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(`${msg} Exceeded max allowed.`);
logger.warn(`${prefix}${msg} Exceeded max allowed.`);
return false;
}
if(waitOnRetry) {
const ms = (4 * 1000) * otherRetryCount;
logger.warn(`${msg} Will wait ${formatNumber(ms / 1000)} seconds before retrying`);
logger.warn(`${prefix}${msg} Will wait ${formatNumber(ms / 1000)} seconds before retrying`);
await sleep(ms);
}
return true;
@@ -1556,7 +1570,7 @@ export const testMaybeStringRegex = (test: string, subject: string, defaultFlags
}
export const isStrongSubredditState = (value: SubredditCriteria | StrongSubredditCriteria) => {
return value.name === undefined || value.name instanceof RegExp;
return value.name === undefined || value.name instanceof RegExp || asSubredditPlaceholder(value.name);
}
export const asStrongSubredditState = (value: any): value is StrongSubredditCriteria => {
@@ -1574,21 +1588,26 @@ export const toStrongSubredditState = (s: SubredditCriteria, opts?: StrongSubred
let nameValOriginallyRegex = false;
let nameReg: RegExp | undefined;
let nameReg: RegExp | undefined | SubredditPlaceholderType;
if (nameValRaw !== undefined) {
if (!(nameValRaw instanceof RegExp)) {
let nameVal = nameValRaw.trim();
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);
if(asSubredditPlaceholder(nameVal)) {
nameReg = subredditPlaceholder;
nameValOriginallyRegex = false;
} else {
nameValOriginallyRegex = true;
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;
}
}
} else {
nameValOriginallyRegex = true;
@@ -2855,7 +2874,7 @@ export const generateSnoowrapEntityFromRedditThing = (data: RedditThing, client:
case 'user':
return new RedditUser({id: data.val}, client, false);
case 'subreddit':
return new Subreddit({name: data.val}, client, false);
return new Subreddit({id: data.val}, client, false);
case 'message':
return new PrivateMessage({id: data.val}, client, false)
@@ -3088,3 +3107,24 @@ 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`;
}