mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-01-14 07:57:57 -05:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
859680dca8 | ||
|
|
18c1ac0fd7 | ||
|
|
2fb503f09f | ||
|
|
7e5eeb71da | ||
|
|
84f2da8b6d | ||
|
|
be51f8ae43 | ||
|
|
f82d985eab | ||
|
|
aab014650a | ||
|
|
113ac3e10e | ||
|
|
afb6aad26d | ||
|
|
33b60825d9 | ||
|
|
b5202a33ac | ||
|
|
ffa1e423b2 | ||
|
|
09cb08492c | ||
|
|
d9ab81ab8c | ||
|
|
98691bd19c | ||
|
|
8123c34463 | ||
|
|
3292d011fa | ||
|
|
661a0ae440 | ||
|
|
05f477b67d | ||
|
|
1317a5916c | ||
|
|
e9135ec1ef | ||
|
|
e58a0f8f21 | ||
|
|
f7cebc013b | ||
|
|
ae8e11feb4 | ||
|
|
e07b8cc291 | ||
|
|
fc51928054 | ||
|
|
e2590e50f8 | ||
|
|
aaed0d3419 | ||
|
|
bc7eff8928 | ||
|
|
d6954533a0 | ||
|
|
ba53233640 | ||
|
|
1ac7ad4724 | ||
|
|
2a282a0d6f | ||
|
|
fd5a92758d | ||
|
|
39daa11f2d | ||
|
|
dac6541e28 | ||
|
|
97906281e6 | ||
|
|
487f13f704 | ||
|
|
631e21452c | ||
|
|
4f3685a1f5 | ||
|
|
d2d945db2c | ||
|
|
910f7f79ef | ||
|
|
a11b667d5e | ||
|
|
885e3fa765 | ||
|
|
465c3c9acf | ||
|
|
161251a943 | ||
|
|
ce4cb96d9a | ||
|
|
c317f95953 | ||
|
|
d0e0515990 | ||
|
|
cdddd8de48 | ||
|
|
f598215d88 | ||
|
|
0c7218571c | ||
|
|
acc7c49e0e | ||
|
|
01839512d5 | ||
|
|
4680640b0c | ||
|
|
b813ebdd96 |
@@ -1,12 +1,32 @@
|
||||
Actions that can submit text (Report, Comment, UserNote) will have their `content` values run through a [Mustache Template](https://mustache.github.io/). This means you can insert data generated by Rules into your text before the Action is performed.
|
||||
Actions that can submit text (Report, Comment, UserNote, Message, Ban, Submission) will have their `content` values run through a [Mustache Template](https://mustache.github.io/). This means you can insert data generated by Rules into your text before the Action is performed.
|
||||
|
||||
See here for a [cheatsheet](https://gist.github.com/FoxxMD/d365707cf99fdb526a504b8b833a5b78) and [here](https://www.tsmean.com/articles/mustache/the-ultimate-mustache-tutorial/) for a more thorough tutorial.
|
||||
|
||||
# Template Data
|
||||
|
||||
Some data can always be accessed at the top-level. Example
|
||||
|
||||
```
|
||||
This action was run from {{manager}} in Check {{check}}.
|
||||
|
||||
The bot intro post is {{botLink}}
|
||||
|
||||
Message the moderators of this subreddit using this [compose link]({{modmailLink}})
|
||||
```
|
||||
|
||||
|
||||
|
||||
| Name | Description | Example |
|
||||
|---------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `manager` | The name of the subreddit the bot is running in | mealtimevideos |
|
||||
| `check` | The name of the Check that was triggered | myCheck |
|
||||
| `botLink` | A link to the bot introduction | https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot |
|
||||
| `modmailLink` | A link that opens reddit's DM compose with the subject line as the Activity being processed | https://www.reddit.com/message/compose?to=/r/mealtimevideos&message=https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot |
|
||||
|
||||
|
||||
## Activity Data
|
||||
|
||||
Activity data can be accessed using the `item` variable. Example
|
||||
**Activity data can be accessed using the `item` variable.** Example
|
||||
|
||||
```
|
||||
This activity is a {{item.kind}} with {{item.votes}} votes, created {{item.age}} ago.
|
||||
@@ -19,14 +39,17 @@ Produces:
|
||||
|
||||
All Actions with `content` have access to this data:
|
||||
|
||||
| Name | Description | Example |
|
||||
|-------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
|
||||
| `kind` | The Activity type (submission or comment) | submission |
|
||||
| `author` | Name of the Author of the Activity being processed | FoxxMD |
|
||||
| `permalink` | URL to the Activity | https://reddit.com/r/mySuibreddit/comments/ab23f/my_post |
|
||||
| `votes` | Number of upvotes | 69 |
|
||||
| `age` | The age of the Activity in a [human friendly format](https://day.js.org/docs/en/durations/humanize) | 5 minutes |
|
||||
| `botLink` | A URL to CM's introduction thread | https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot |
|
||||
| Name | Description | Example |
|
||||
|--------------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------|
|
||||
| `kind` | The Activity type (submission or comment) | submission |
|
||||
| `author` | Name of the Author of the Activity being processed | FoxxMD |
|
||||
| `permalink` | URL to the Activity | https://reddit.com/r/mySuibreddit/comments/ab23f/my_post |
|
||||
| `votes` | Number of upvotes | 69 |
|
||||
| `age` | The age of the Activity in a [human friendly format](https://day.js.org/docs/en/durations/humanize) | 5 minutes |
|
||||
| `subreddit` | The name of the subreddit the Activity is from | mealtimevideos |
|
||||
| `id` | The `Reddit Thing` ID for the Activity | t3_0tin1 |
|
||||
| `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... |
|
||||
|
||||
### Submissions
|
||||
|
||||
@@ -89,7 +112,39 @@ Produces
|
||||
|
||||
> Submission was repeated 7 times
|
||||
|
||||
#### Quick Templating Tutorial
|
||||
## Action Data
|
||||
|
||||
### Summary
|
||||
|
||||
A summary of what actions have already been run **when the template is rendered** is available using the `actionSummary` variable. It is therefore important that the Action you want to produce the summary is run **after** any other Actions you want to get a summary for.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
A summary of actions processed for this activity, so far:
|
||||
|
||||
{{actionSummary}}
|
||||
```
|
||||
|
||||
Would produce:
|
||||
> A summary of actions processed for this activity, so far:
|
||||
>
|
||||
> * approve - ✘ - Item is already approved??
|
||||
> * lock - ✓
|
||||
> * modnote - ✓ - (SOLID_CONTRIBUTOR) User is good
|
||||
|
||||
### Individual
|
||||
|
||||
Individual **Actions** can be accessed using the name of the action, **lower-cased, with all spaces/dashes/underscores.** Example:
|
||||
|
||||
```
|
||||
User was banned for {{actions.exampleban.duration}} for {{actions.exampleban.reason}}
|
||||
```
|
||||
Produces
|
||||
|
||||
> User was banned for 4 days for toxic behavior
|
||||
|
||||
# Quick Templating Tutorial
|
||||
|
||||
As a quick example for how you will most likely be using templating -- wrapping a variable in curly brackets, `{{variable}}`, will cause the variable value to be rendered instead of the brackets:
|
||||
|
||||
|
||||
@@ -692,15 +692,16 @@ Some other things to note:
|
||||
* If the `to` property is not specified then the message is sent to the Author of the Activity being processed
|
||||
* `to` may be a **User** (u/aUser) or a **Subreddit** (r/aSubreddit)
|
||||
* `to` **cannot** be a Subreddit when `asSubreddit: true` -- IE cannot send subreddit-to-subreddit messages
|
||||
* `content` can be [templated](#templating) and use [URL Tokens](#url-tokens)
|
||||
* TIP: `to` can be templated -- to send a message to the subreddit the Activity being processed is in use `'r/{{item.subreddit}}'`
|
||||
* `content` and `title` can be [templated](#templating) and use [URL Tokens](#url-tokens)
|
||||
|
||||
```yaml
|
||||
actions:
|
||||
- kind: message
|
||||
asSubreddit: true
|
||||
content: 'A message sent as the subreddit'
|
||||
title: 'Title of the message'
|
||||
to: 'u/aUser' # do not specify 'to' in order default to sending to Author of Activity being processed
|
||||
content: 'A message sent as the subreddit' # can be templated
|
||||
title: 'Title of the message' # can be templated
|
||||
to: 'u/aUser' # do not specify 'to' in order default to sending to Author of Activity being processed. Can also be templated
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
// for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
{
|
||||
"flairText": ["Supreme Memer"],
|
||||
"names": ["user1","user2"]
|
||||
"name": ["user1","user2"]
|
||||
},
|
||||
{
|
||||
// for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
|
||||
@@ -30,7 +30,7 @@ runs:
|
||||
# for this to pass the Author of the Submission must not have the flair "Supreme Memer" and have the name "user1" or "user2"
|
||||
- flairText:
|
||||
- Supreme Memer
|
||||
names:
|
||||
name:
|
||||
- user1
|
||||
- user2
|
||||
# for this to pass the Author of the Submission must not have the flair "Decent Memer"
|
||||
|
||||
@@ -3,7 +3,7 @@ import LockAction, {LockActionJson} from "./LockAction";
|
||||
import {RemoveAction, RemoveActionJson} from "./RemoveAction";
|
||||
import {ReportAction, ReportActionJson} from "./ReportAction";
|
||||
import {FlairAction, FlairActionJson} from "./SubmissionAction/FlairAction";
|
||||
import Action, {ActionJson, StructuredActionJson} from "./index";
|
||||
import Action, {ActionJson, ActionRuntimeOptions, StructuredActionJson} from "./index";
|
||||
import {Logger} from "winston";
|
||||
import {UserNoteAction, UserNoteActionJson} from "./UserNoteAction";
|
||||
import ApproveAction, {ApproveActionConfig} from "./ApproveAction";
|
||||
@@ -21,38 +21,38 @@ import {ModNoteAction, ModNoteActionJson} from "./ModNoteAction";
|
||||
import {SubmissionAction, SubmissionActionJson} from "./SubmissionAction";
|
||||
|
||||
export function actionFactory
|
||||
(config: StructuredActionJson, logger: Logger, subredditName: string, resources: SubredditResources, client: ExtendedSnoowrap, emitter: EventEmitter): Action {
|
||||
(config: StructuredActionJson, runtimeOptions: ActionRuntimeOptions): Action {
|
||||
switch (config.kind) {
|
||||
case 'comment':
|
||||
return new CommentAction({...config as StructuredFilter<CommentActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new CommentAction({...config as StructuredFilter<CommentActionJson>, ...runtimeOptions});
|
||||
case 'submission':
|
||||
return new SubmissionAction({...config as StructuredFilter<SubmissionActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new SubmissionAction({...config as StructuredFilter<SubmissionActionJson>, ...runtimeOptions});
|
||||
case 'lock':
|
||||
return new LockAction({...config as StructuredFilter<LockActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new LockAction({...config as StructuredFilter<LockActionJson>, ...runtimeOptions});
|
||||
case 'remove':
|
||||
return new RemoveAction({...config as StructuredFilter<RemoveActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new RemoveAction({...config as StructuredFilter<RemoveActionJson>, ...runtimeOptions});
|
||||
case 'report':
|
||||
return new ReportAction({...config as StructuredFilter<ReportActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new ReportAction({...config as StructuredFilter<ReportActionJson>, ...runtimeOptions});
|
||||
case 'flair':
|
||||
return new FlairAction({...config as StructuredFilter<FlairActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new FlairAction({...config as StructuredFilter<FlairActionJson>, ...runtimeOptions});
|
||||
case 'userflair':
|
||||
return new UserFlairAction({...config as StructuredFilter<UserFlairActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new UserFlairAction({...config as StructuredFilter<UserFlairActionJson>, ...runtimeOptions});
|
||||
case 'approve':
|
||||
return new ApproveAction({...config as StructuredFilter<ApproveActionConfig>, logger, subredditName, resources, client, emitter});
|
||||
return new ApproveAction({...config as StructuredFilter<ApproveActionConfig>, ...runtimeOptions});
|
||||
case 'usernote':
|
||||
return new UserNoteAction({...config as StructuredFilter<UserNoteActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new UserNoteAction({...config as StructuredFilter<UserNoteActionJson>, ...runtimeOptions});
|
||||
case 'ban':
|
||||
return new BanAction({...config as StructuredFilter<BanActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new BanAction({...config as StructuredFilter<BanActionJson>, ...runtimeOptions});
|
||||
case 'message':
|
||||
return new MessageAction({...config as StructuredFilter<MessageActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new MessageAction({...config as StructuredFilter<MessageActionJson>, ...runtimeOptions});
|
||||
case 'dispatch':
|
||||
return new DispatchAction({...config as StructuredFilter<DispatchActionJson>, logger, subredditName, resources, client, emitter});
|
||||
return new DispatchAction({...config as StructuredFilter<DispatchActionJson>, ...runtimeOptions});
|
||||
case 'cancelDispatch':
|
||||
return new CancelDispatchAction({...config as StructuredFilter<CancelDispatchActionJson>, logger, subredditName, resources, client, emitter})
|
||||
return new CancelDispatchAction({...config as StructuredFilter<CancelDispatchActionJson>, ...runtimeOptions})
|
||||
case 'contributor':
|
||||
return new ContributorAction({...config as StructuredFilter<ContributorActionJson>, logger, subredditName, resources, client, emitter})
|
||||
return new ContributorAction({...config as StructuredFilter<ContributorActionJson>, ...runtimeOptions})
|
||||
case 'modnote':
|
||||
return new ModNoteAction({...config as StructuredFilter<ModNoteActionJson>, logger, subredditName, resources, client, emitter})
|
||||
return new ModNoteAction({...config as StructuredFilter<ModNoteActionJson>, ...runtimeOptions})
|
||||
default:
|
||||
throw new Error('rule "kind" was not recognized.');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTarget, ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {asComment, asSubmission} from "../util";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class ApproveAction extends Action {
|
||||
|
||||
@@ -26,7 +27,7 @@ export class ApproveAction extends Action {
|
||||
this.targets = targets;
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const touchedEntities = [];
|
||||
|
||||
|
||||
@@ -7,10 +7,18 @@ import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {truncateStringToLength} from "../util";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
const truncate = truncateStringToLength(100);
|
||||
const truncateLongMessage = truncateStringToLength(200);
|
||||
|
||||
const truncateIfNotUndefined = (val: string | undefined) => {
|
||||
if(val === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return truncate(val);
|
||||
}
|
||||
|
||||
export class BanAction extends Action {
|
||||
|
||||
message?: string;
|
||||
@@ -39,13 +47,13 @@ export class BanAction extends Action {
|
||||
return 'ban';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const renderedBody = this.message === undefined ? undefined : await this.resources.renderContent(this.message, item, ruleResults);
|
||||
const renderedContent = renderedBody === undefined ? undefined : `${renderedBody}${await this.resources.generateFooter(item, this.footer)}`;
|
||||
const renderedBody = await this.renderContent(this.message, item, ruleResults, actionResults);
|
||||
const renderedContent = renderedBody === undefined ? undefined : `${renderedBody}${await this.resources.renderFooter(item, this.footer)}`;
|
||||
|
||||
const renderedReason = this.reason === undefined ? undefined : truncate(await this.resources.renderContent(this.reason, item, ruleResults));
|
||||
const renderedNote = this.note === undefined ? undefined : truncate(await this.resources.renderContent(this.note, item, ruleResults));
|
||||
const renderedReason = truncateIfNotUndefined(await this.renderContent(this.reason, item, ruleResults, actionResults) as string);
|
||||
const renderedNote = truncateIfNotUndefined(await this.renderContent(this.note, item, ruleResults, actionResults) as string);
|
||||
|
||||
const touchedEntities = [];
|
||||
let banPieces = [];
|
||||
@@ -72,7 +80,13 @@ export class BanAction extends Action {
|
||||
dryRun,
|
||||
success: true,
|
||||
result: `Banned ${item.author.name} ${durText}${renderedReason !== undefined ? ` (${renderedReason})` : ''}`,
|
||||
touchedEntities
|
||||
touchedEntities,
|
||||
data: {
|
||||
message: renderedContent === undefined ? undefined : renderedContent,
|
||||
reason: renderedReason,
|
||||
note: renderedNote,
|
||||
duration: durText
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {isSubmission, parseDurationValToDuration} from "../util";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTarget, ActionTypes, InclusiveActionTarget} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class CancelDispatchAction extends Action {
|
||||
identifiers?: (string | null)[];
|
||||
@@ -35,7 +36,7 @@ export class CancelDispatchAction extends Action {
|
||||
this.targets = !Array.isArray(target) ? [target] : target;
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
// see note in DispatchAction about missing runtimeDryrun
|
||||
const dryRun = this.dryRun;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTarget, ActionTypes, ArbitraryActionTarget} from "../Common/Infrastructure/Atomic";
|
||||
import {CMError} from "../Utils/Errors";
|
||||
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class CommentAction extends Action {
|
||||
content: string;
|
||||
@@ -44,12 +45,11 @@ export class CommentAction extends Action {
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const content = await this.resources.getContent(this.content, item.subreddit);
|
||||
const body = await renderContent(content, item, ruleResults, this.resources.userNotes);
|
||||
const body = await this.renderContent(this.content, item, ruleResults, actionResults) as string;
|
||||
|
||||
const footer = await this.resources.generateFooter(item, this.footer);
|
||||
const footer = await this.resources.renderFooter(item, this.footer);
|
||||
|
||||
const renderedContent = `${body}${footer}`;
|
||||
this.logger.verbose(`Contents:\r\n${renderedContent.length > 100 ? `\r\n${renderedContent}` : renderedContent}`);
|
||||
@@ -154,6 +154,12 @@ export class CommentAction extends Action {
|
||||
success: !allErrors,
|
||||
result: `${targetResults.join('\n')}${truncateStringToLength(100)(body)}`,
|
||||
touchedEntities,
|
||||
data: {
|
||||
body,
|
||||
bodyShort: truncateStringToLength(100)(body),
|
||||
comments: targetResults,
|
||||
commentsFormatted: targetResults.map(x => `* ${x}`).join('\n')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import Comment from "snoowrap/dist/objects/Comment";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTarget, ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class ContributorAction extends Action {
|
||||
|
||||
@@ -25,7 +26,7 @@ export class ContributorAction extends Action {
|
||||
this.actionType = action;
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
|
||||
const contributors = await this.resources.getSubredditContributors();
|
||||
|
||||
@@ -8,6 +8,7 @@ import {activityDispatchConfigToDispatch, isSubmission, parseDurationValToDurati
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTarget, ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class DispatchAction extends Action {
|
||||
dispatchData: ActivityDispatchConfig;
|
||||
@@ -39,7 +40,7 @@ export class DispatchAction extends Action {
|
||||
this.targets = !Array.isArray(target) ? [target] : target;
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
// ignore runtimeDryrun here because "real run" isn't causing any reddit api calls to happen
|
||||
// -- basically if bot is in dryrun this should still run since we want the "full effect" of the bot
|
||||
// BUT if the action explicitly sets 'dryRun: true' then do not dispatch as they probably don't want to it actually going (intention?)
|
||||
|
||||
@@ -5,13 +5,14 @@ import {ActionProcessResult, RuleResult} from "../Common/interfaces";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class LockAction extends Action {
|
||||
getKind(): ActionTypes {
|
||||
return 'lock';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const touchedEntities = [];
|
||||
//snoowrap typing issue, thinks comments can't be locked
|
||||
|
||||
@@ -16,6 +16,7 @@ import {ErrorWithCause} from "pony-cause";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class MessageAction extends Action {
|
||||
content: string;
|
||||
@@ -48,28 +49,30 @@ export class MessageAction extends Action {
|
||||
return 'message';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
|
||||
const body = await this.resources.renderContent(this.content, item, ruleResults);
|
||||
const subject = this.title === undefined ? `Concerning your ${isSubmission(item) ? 'Submission' : 'Comment'}` : await this.resources.renderContent(this.title, item, ruleResults);
|
||||
const body = await this.renderContent(this.content, item, ruleResults, actionResults);
|
||||
const titleTemplate = this.title ?? `Concerning your ${isSubmission(item) ? 'Submission' : 'Comment'}`;
|
||||
const subject = await this.renderContent(titleTemplate, item, ruleResults, actionResults) as string;
|
||||
|
||||
const footer = await this.resources.generateFooter(item, this.footer);
|
||||
const footer = await this.resources.renderFooter(item, this.footer);
|
||||
|
||||
const renderedContent = `${body}${footer}`;
|
||||
|
||||
let recipient = item.author.name;
|
||||
if(this.to !== undefined) {
|
||||
const renderedTo = await this.renderContent(this.to, item, ruleResults, actionResults) as string;
|
||||
// parse to value
|
||||
try {
|
||||
const entityData = parseRedditEntity(this.to, 'user');
|
||||
const entityData = parseRedditEntity(renderedTo, 'user');
|
||||
if(entityData.type === 'user') {
|
||||
recipient = entityData.name;
|
||||
} else {
|
||||
recipient = `/r/${entityData.name}`;
|
||||
}
|
||||
} catch (err: any) {
|
||||
throw new ErrorWithCause(`'to' field for message was not in a valid format. See ${REDDIT_ENTITY_REGEX_URL} for valid examples`, {cause: err});
|
||||
throw new ErrorWithCause(`'to' field for message was not in a valid format, given value after templating: ${renderedTo} -- See ${REDDIT_ENTITY_REGEX_URL} for valid examples`, {cause: err});
|
||||
}
|
||||
if(recipient.includes('/r/') && this.asSubreddit) {
|
||||
throw new SimpleError(`Cannot send a message as a subreddit to another subreddit. Requested recipient: ${recipient}`);
|
||||
@@ -123,7 +126,7 @@ export interface MessageActionConfig extends RequiredRichContent, Footer {
|
||||
asSubreddit: boolean
|
||||
|
||||
/**
|
||||
* Entity to send message to.
|
||||
* Entity to send message to. It can be templated.
|
||||
*
|
||||
* If not present Message be will sent to the Author of the Activity being checked.
|
||||
*
|
||||
@@ -135,8 +138,9 @@ export interface MessageActionConfig extends RequiredRichContent, Footer {
|
||||
*
|
||||
* **Note:** Reddit does not support sending a message AS a subreddit TO another subreddit
|
||||
*
|
||||
* @pattern ^\s*(\/[ru]\/|[ru]\/)*(\w+)*\s*$
|
||||
* @examples ["aUserName","u/aUserName","r/aSubreddit"]
|
||||
* **Tip:** To send a message to the subreddit of the Activity us `to: 'r/{{item.subreddit}}'`
|
||||
*
|
||||
* @examples ["aUserName","u/aUserName","r/aSubreddit", "r/{{item.subreddit}}"]
|
||||
* */
|
||||
to?: string
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes, ModUserNoteLabel} from "../Common/Infrastructure/Atomic";
|
||||
import {ModNote} from "../Subreddit/ModNotes/ModNote";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
|
||||
export class ModNoteAction extends Action {
|
||||
@@ -39,13 +40,12 @@ export class ModNoteAction extends Action {
|
||||
}
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
|
||||
const modLabel = this.type !== undefined ? toModNoteLabel(this.type) : undefined;
|
||||
|
||||
const content = await this.resources.getContent(this.content, item.subreddit);
|
||||
const renderedContent = await renderContent(content, item, ruleResults, this.resources.userNotes);
|
||||
const renderedContent = await this.renderContent(this.content, item, ruleResults, actionResults);
|
||||
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
|
||||
|
||||
// TODO see what changes are made for bulk fetch of notes before implementing this
|
||||
|
||||
@@ -8,6 +8,7 @@ import {isSubmission, truncateStringToLength} from "../util";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
const truncate = truncateStringToLength(100);
|
||||
export class RemoveAction extends Action {
|
||||
@@ -31,7 +32,7 @@ export class RemoveAction extends Action {
|
||||
this.reasonId = reasonId;
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const touchedEntities = [];
|
||||
let removeSummary = [];
|
||||
@@ -44,7 +45,7 @@ export class RemoveAction extends Action {
|
||||
removeSummary.push('Marked as SPAM');
|
||||
this.logger.verbose('Marking as spam on removal');
|
||||
}
|
||||
const renderedNote = this.note === undefined ? undefined : await this.resources.renderContent(this.note, item, ruleResults);
|
||||
const renderedNote = await this.renderContent(this.note, item, ruleResults, actionResults);
|
||||
let foundReasonId: string | undefined;
|
||||
let foundReason: string | undefined;
|
||||
|
||||
@@ -99,7 +100,8 @@ export class RemoveAction extends Action {
|
||||
return {
|
||||
dryRun,
|
||||
success: true,
|
||||
touchedEntities
|
||||
touchedEntities,
|
||||
result: removeSummary.join(' | ')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {ActionProcessResult, RichContent, RuleResult} from "../Common/interfaces
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
// https://www.reddit.com/dev/api/oauth#POST_api_report
|
||||
// denotes 100 characters maximum
|
||||
@@ -25,10 +26,9 @@ export class ReportAction extends Action {
|
||||
return 'report';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const content = await this.resources.getContent(this.content, item.subreddit);
|
||||
const renderedContent = await renderContent(content, item, ruleResults, this.resources.userNotes);
|
||||
const renderedContent = (await this.renderContent(this.content, item, ruleResults, actionResults) as string);
|
||||
this.logger.verbose(`Contents:\r\n${renderedContent}`);
|
||||
const truncatedContent = reportTrunc(renderedContent);
|
||||
const touchedEntities = [];
|
||||
|
||||
@@ -10,6 +10,7 @@ import {ActionTarget, ActionTypes, ArbitraryActionTarget} from "../Common/Infras
|
||||
import {CMError} from "../Utils/Errors";
|
||||
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
|
||||
import Subreddit from "snoowrap/dist/objects/Subreddit";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class SubmissionAction extends Action {
|
||||
content?: string;
|
||||
@@ -67,21 +68,21 @@ export class SubmissionAction extends Action {
|
||||
return 'submission';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
|
||||
const title = await this.resources.renderContent(this.title, item, ruleResults);
|
||||
const title = await this.renderContent(this.title, item, ruleResults, actionResults) as string;
|
||||
this.logger.verbose(`Title: ${title}`);
|
||||
|
||||
const url = this.url !== undefined ? await this.resources.renderContent(this.url, item, ruleResults) : undefined;
|
||||
const url = await this.renderContent(this.url, item, ruleResults, actionResults);
|
||||
|
||||
this.logger.verbose(`URL: ${url !== undefined ? url : '[No URL]'}`);
|
||||
|
||||
const body = this.content !== undefined ? await this.resources.renderContent(this.content, item, ruleResults) : undefined;
|
||||
const body = await this.renderContent(this.content, item, ruleResults, actionResults);
|
||||
|
||||
let renderedContent: string | undefined = undefined;
|
||||
if(body !== undefined) {
|
||||
const footer = await this.resources.generateFooter(item, this.footer);
|
||||
const footer = await this.resources.renderFooter(item, this.footer);
|
||||
renderedContent = `${body}${footer}`;
|
||||
this.logger.verbose(`Contents:\r\n${renderedContent.length > 100 ? `\r\n${renderedContent}` : renderedContent}`);
|
||||
} else {
|
||||
@@ -204,6 +205,11 @@ export class SubmissionAction extends Action {
|
||||
success: !allErrors,
|
||||
result: `${targetResults.join('\n')}${this.url !== undefined ? `\nURL: ${this.url}` : ''}${body !== undefined ? truncateStringToLength(100)(body) : ''}`,
|
||||
touchedEntities,
|
||||
data: {
|
||||
body,
|
||||
bodyShort: body !== undefined ? truncateStringToLength(100)(body) : '',
|
||||
submissions: targetResults.map(x => `* ${x}`).join('\n')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -309,7 +315,7 @@ export interface SubmissionActionConfig extends RichContent, Footer {
|
||||
* * 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed
|
||||
* * [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos
|
||||
* */
|
||||
targets?: 'self' | string
|
||||
targets?: ('self' | string) | ('self' | string)[]
|
||||
}
|
||||
|
||||
export interface SubmissionActionOptions extends SubmissionActionConfig, ActionOptions {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Comment from 'snoowrap/dist/objects/Comment';
|
||||
import {RuleResultEntity} from "../../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../../Subreddit/Manager";
|
||||
import {ActionTypes} from "../../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class FlairAction extends Action {
|
||||
text: string;
|
||||
@@ -26,7 +27,7 @@ export class FlairAction extends Action {
|
||||
return 'flair';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
let flairParts = [];
|
||||
if(this.text !== '') {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {ActionProcessResult, RuleResult} from '../Common/interfaces';
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export class UserFlairAction extends Action {
|
||||
text?: string;
|
||||
@@ -22,7 +23,7 @@ export class UserFlairAction extends Action {
|
||||
return 'userflair';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
let flairParts = [];
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {ActionProcessResult, RuleResult} from "../Common/interfaces";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {runCheckOptions} from "../Subreddit/Manager";
|
||||
import {ActionTypes, UserNoteType} from "../Common/Infrastructure/Atomic";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
|
||||
export class UserNoteAction extends Action {
|
||||
@@ -27,10 +28,9 @@ export class UserNoteAction extends Action {
|
||||
return 'usernote';
|
||||
}
|
||||
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
async process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult> {
|
||||
const dryRun = this.getRuntimeAwareDryrun(options);
|
||||
const content = await this.resources.getContent(this.content, item.subreddit);
|
||||
const renderedContent = await renderContent(content, item, ruleResults, this.resources.userNotes);
|
||||
const renderedContent = (await this.renderContent(this.content, item, ruleResults, actionResults) as string);
|
||||
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
|
||||
|
||||
if (!this.allowDuplicate) {
|
||||
|
||||
@@ -19,6 +19,8 @@ import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
import {FindOptionsWhere} from "typeorm/find-options/FindOptionsWhere";
|
||||
import {ActionTypes} from "../Common/Infrastructure/Atomic";
|
||||
import {RunnableBaseJson, RunnableBaseOptions, StructuredRunnableBase} from "../Common/Infrastructure/Runnable";
|
||||
import { SubredditResources } from "../Subreddit/SubredditResources";
|
||||
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
|
||||
|
||||
export abstract class Action extends RunnableBase {
|
||||
name?: string;
|
||||
@@ -29,6 +31,8 @@ export abstract class Action extends RunnableBase {
|
||||
managerEmitter: EventEmitter;
|
||||
// actionEntity: ActionEntity | null = null;
|
||||
actionPremiseEntity: ActionPremise | null = null;
|
||||
checkName: string;
|
||||
subredditName: string;
|
||||
|
||||
constructor(options: ActionOptions) {
|
||||
super(options);
|
||||
@@ -40,6 +44,7 @@ export abstract class Action extends RunnableBase {
|
||||
subredditName,
|
||||
dryRun = false,
|
||||
emitter,
|
||||
checkName,
|
||||
} = options;
|
||||
|
||||
this.name = name;
|
||||
@@ -48,6 +53,8 @@ export abstract class Action extends RunnableBase {
|
||||
this.client = client;
|
||||
this.logger = logger.child({labels: [`Action ${this.getActionUniqueName()}`]}, mergeArr);
|
||||
this.managerEmitter = emitter;
|
||||
this.checkName = checkName;
|
||||
this.subredditName = subredditName;
|
||||
}
|
||||
|
||||
abstract getKind(): ActionTypes;
|
||||
@@ -112,7 +119,7 @@ export abstract class Action extends RunnableBase {
|
||||
}
|
||||
}
|
||||
|
||||
async handle(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionResultEntity> {
|
||||
async handle(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionResultEntity> {
|
||||
const {dryRun: runtimeDryrun} = options;
|
||||
const dryRun = runtimeDryrun || this.dryRun;
|
||||
|
||||
@@ -148,10 +155,11 @@ export abstract class Action extends RunnableBase {
|
||||
actRes.runReason = runReason;
|
||||
return actRes;
|
||||
}
|
||||
const results = await this.process(item, ruleResults, options);
|
||||
const results = await this.process(item, ruleResults, actionResults, options);
|
||||
actRes.success = results.success;
|
||||
actRes.dryRun = results.dryRun;
|
||||
actRes.result = results.result;
|
||||
actRes.data = results.data;
|
||||
actRes.touchedEntities = results.touchedEntities ?? [];
|
||||
|
||||
return actRes;
|
||||
@@ -166,20 +174,31 @@ export abstract class Action extends RunnableBase {
|
||||
}
|
||||
}
|
||||
|
||||
abstract process(item: Comment | Submission, ruleResults: RuleResultEntity[], options: runCheckOptions): Promise<ActionProcessResult>;
|
||||
abstract process(item: Comment | Submission, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[], options: runCheckOptions): Promise<ActionProcessResult>;
|
||||
|
||||
getRuntimeAwareDryrun(options: runCheckOptions): boolean {
|
||||
const {dryRun: runtimeDryrun} = options;
|
||||
return runtimeDryrun || this.dryRun;
|
||||
}
|
||||
|
||||
async renderContent(template: string | undefined, item: SnoowrapActivity, ruleResults: RuleResultEntity[], actionResults: ActionResultEntity[]): Promise<string | undefined> {
|
||||
if(template === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return await this.resources.renderContent(template, item, ruleResults, actionResults, {manager: this.subredditName, check: this.checkName});
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActionOptions extends Omit<ActionConfig, 'authorIs' | 'itemIs'>, RunnableBaseOptions {
|
||||
//logger: Logger;
|
||||
subredditName: string;
|
||||
//resources: SubredditResources;
|
||||
export interface ActionRuntimeOptions {
|
||||
checkName: string
|
||||
subredditName: string
|
||||
client: ExtendedSnoowrap;
|
||||
emitter: EventEmitter
|
||||
emitter: EventEmitter;
|
||||
resources: SubredditResources;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface ActionOptions extends Omit<ActionConfig, 'authorIs' | 'itemIs'>, RunnableBaseOptions, ActionRuntimeOptions {
|
||||
}
|
||||
|
||||
export interface ActionConfig extends RunnableBaseJson {
|
||||
|
||||
@@ -386,7 +386,7 @@ class Bot implements BotInstanceFunctions {
|
||||
async testClient(initial = true) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const user = this.client.getMe().fetch();
|
||||
const user = await this.client.getMe().fetch();
|
||||
this.logger.info('Test API call successful');
|
||||
return user;
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -222,7 +222,14 @@ export abstract class Check extends RunnableBase implements Omit<ICheck, 'postTr
|
||||
this.actions.push(actionFactory({
|
||||
...aj,
|
||||
dryRun: this.dryRun || aj.dryRun
|
||||
}, this.logger, subredditName, this.resources, this.client, this.emitter));
|
||||
}, {
|
||||
logger: this.logger,
|
||||
subredditName,
|
||||
resources: this.resources,
|
||||
client: this.client,
|
||||
emitter: this.emitter,
|
||||
checkName: this.name
|
||||
}));
|
||||
// @ts-ignore
|
||||
a.logger = this.logger;
|
||||
} else {
|
||||
@@ -564,7 +571,7 @@ export abstract class Check extends RunnableBase implements Omit<ICheck, 'postTr
|
||||
const dr = dryRun || this.dryRun;
|
||||
this.logger.debug(`${dr ? 'DRYRUN - ' : ''}Running Actions`);
|
||||
for (const a of this.actions) {
|
||||
const res = await a.handle(item, ruleResults, options);
|
||||
const res = await a.handle(item, ruleResults, runActions, options);
|
||||
runActions.push(res);
|
||||
}
|
||||
this.logger.info(`${dr ? 'DRYRUN - ' : ''}Ran Actions: ${runActions.map(x => x.premise.getFriendlyIdentifier()).join(' | ')}`);
|
||||
|
||||
@@ -56,6 +56,11 @@ export class ActionResultEntity extends TimeAwareRandomBaseEntity {
|
||||
@JoinColumn({name: 'premiseId'})
|
||||
premise!: ActionPremise;
|
||||
|
||||
/**
|
||||
* Ephemeral -- only added during actual run time and used for action templating. Is not available after loading from DB.
|
||||
* */
|
||||
data?: any;
|
||||
|
||||
touchedEntities: (Submission | Comment | RedditUser | string)[] = []
|
||||
|
||||
set itemIs(data: ActivityStateFilterResult | IFilterResult<TypedActivityState> | undefined) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {ActivityType} from "./Reddit";
|
||||
|
||||
/**
|
||||
* A duration and how to compare it against a value
|
||||
*
|
||||
@@ -284,3 +286,84 @@ export interface ImageHashCacheData {
|
||||
original?: string
|
||||
flipped?: string
|
||||
}
|
||||
|
||||
// https://www.reddit.com/message/compose?to=/r/mealtimevideos&message=https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot
|
||||
|
||||
export interface BaseTemplateData {
|
||||
botLink: string
|
||||
modmailLink?: string
|
||||
manager?: string
|
||||
check?: string
|
||||
//[key: string]: any
|
||||
}
|
||||
|
||||
export interface ActivityTemplateData {
|
||||
kind: ActivityType
|
||||
author: string
|
||||
votes: number
|
||||
age: string
|
||||
permalink: string
|
||||
id: string
|
||||
subreddit: string
|
||||
title: string
|
||||
shortTitle: string
|
||||
}
|
||||
|
||||
export interface ModdedActivityTemplateData {
|
||||
reports: number
|
||||
modReports: number
|
||||
userReports: number
|
||||
}
|
||||
|
||||
export interface SubmissionTemplateData extends ActivityTemplateData, Partial<ModdedActivityTemplateData> {
|
||||
nsfw: boolean
|
||||
spoiler: boolean
|
||||
op: boolean
|
||||
upvoteRatio: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface CommentTemplateData extends ActivityTemplateData, Partial<ModdedActivityTemplateData> {
|
||||
op: boolean
|
||||
}
|
||||
|
||||
export interface SubredditTemplateData {
|
||||
subredditBreakdownFormatted: string
|
||||
subredditBreakdown?: {
|
||||
totalFormatted: string
|
||||
submissionFormatted: string
|
||||
commentFormatted: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuleResultTemplateData {
|
||||
kind: string
|
||||
triggered: boolean
|
||||
result: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ActionResultTemplateData {
|
||||
kind: string
|
||||
success: boolean
|
||||
result: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ActionResultsTemplateData {
|
||||
actionSummary: string
|
||||
actions: {
|
||||
[key: string]: ActionResultTemplateData
|
||||
}
|
||||
}
|
||||
|
||||
export interface RuleResultsTemplateData {
|
||||
ruleSummary: string
|
||||
rules: {
|
||||
[key: string]: RuleResultTemplateData
|
||||
}
|
||||
}
|
||||
|
||||
export interface GenericContentTemplateData extends BaseTemplateData, Partial<RuleResultsTemplateData>, Partial<ActionResultsTemplateData> {
|
||||
item?: (SubmissionTemplateData | CommentTemplateData)
|
||||
}
|
||||
|
||||
@@ -88,3 +88,18 @@ export interface SubredditRemovalReason {
|
||||
id: string,
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface SubredditActivityAbsoluteBreakdown {
|
||||
count: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface SubredditActivityBreakdown extends SubredditActivityAbsoluteBreakdown {
|
||||
percent: number
|
||||
}
|
||||
|
||||
export interface SubredditActivityBreakdownByType {
|
||||
total: SubredditActivityBreakdown[]
|
||||
submission: SubredditActivityBreakdown[]
|
||||
comment: SubredditActivityBreakdown[]
|
||||
}
|
||||
|
||||
@@ -42,4 +42,4 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
|
||||
export const defaultDataDir = path.resolve(__dirname, '../..');
|
||||
export const defaultConfigFilenames = ['config.json', 'config.yaml'];
|
||||
|
||||
export const VERSION = '0.12.0';
|
||||
export const VERSION = '0.12.1';
|
||||
|
||||
@@ -1730,6 +1730,7 @@ export interface ActionProcessResult {
|
||||
dryRun: boolean,
|
||||
result?: string
|
||||
touchedEntities?: (Submission | Comment | RedditUser | string)[]
|
||||
data?: any
|
||||
}
|
||||
|
||||
export interface EventActivity {
|
||||
|
||||
@@ -7,9 +7,10 @@ import {Rule, RuleJSONConfig, RuleOptions} from "./index";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
asComment,
|
||||
asSubmission,
|
||||
FAIL,
|
||||
formatNumber, getActivitySubredditName, historyFilterConfigToOptions, isSubmission,
|
||||
formatNumber, getActivitySubredditName, historyFilterConfigToOptions, isComment, isSubmission,
|
||||
parseSubredditName,
|
||||
PASS,
|
||||
percentFromString, removeUndefinedKeys, toStrongSubredditState, windowConfigToWindowCriteria
|
||||
@@ -20,6 +21,7 @@ import {CompareValueOrPercent} from "../Common/Infrastructure/Atomic";
|
||||
import {ActivityWindowConfig, ActivityWindowCriteria} from "../Common/Infrastructure/ActivityWindow";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
|
||||
import {getSubredditBreakdownByActivityType} from "../Utils/SnoowrapUtils";
|
||||
|
||||
export interface CommentThresholdCriteria extends ThresholdCriteria {
|
||||
/**
|
||||
@@ -206,10 +208,11 @@ export class HistoryRule extends Rule {
|
||||
fOpTotal = filteredCounts.opTotal;
|
||||
}
|
||||
|
||||
let asOp = false;
|
||||
let commentTrigger = undefined;
|
||||
if(comment !== undefined) {
|
||||
const {operator, value, isPercent, extra = ''} = parseGenericValueOrPercentComparison(comment);
|
||||
const asOp = extra.toLowerCase().includes('op');
|
||||
asOp = extra.toLowerCase().includes('op');
|
||||
if(isPercent) {
|
||||
const per = value / 100;
|
||||
if(asOp) {
|
||||
@@ -264,7 +267,8 @@ export class HistoryRule extends Rule {
|
||||
submissionTrigger,
|
||||
commentTrigger,
|
||||
totalTrigger,
|
||||
triggered: (submissionTrigger === undefined || submissionTrigger === true) && (commentTrigger === undefined || commentTrigger === true) && (totalTrigger === undefined || totalTrigger === true)
|
||||
triggered: (submissionTrigger === undefined || submissionTrigger === true) && (commentTrigger === undefined || commentTrigger === true) && (totalTrigger === undefined || totalTrigger === true),
|
||||
subredditBreakdown: getSubredditBreakdownByActivityType(!asOp ? filteredActivities : filteredActivities.filter(x => asSubmission(x) || x.is_submitter))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,6 +324,7 @@ export class HistoryRule extends Rule {
|
||||
submissionTrigger,
|
||||
commentTrigger,
|
||||
totalTrigger,
|
||||
subredditBreakdown,
|
||||
} = results;
|
||||
|
||||
const data: any = {
|
||||
@@ -338,6 +343,7 @@ export class HistoryRule extends Rule {
|
||||
submissionTrigger,
|
||||
commentTrigger,
|
||||
totalTrigger,
|
||||
subredditBreakdown
|
||||
};
|
||||
|
||||
let thresholdSummary = [];
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import {ActivityWindow, ActivityWindowConfig} from "../Common/Infrastructure/ActivityWindow";
|
||||
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
|
||||
import {ImageHashCacheData} from "../Common/Infrastructure/Atomic";
|
||||
import {getSubredditBreakdownByActivityType} from "../Utils/SnoowrapUtils";
|
||||
|
||||
const parseLink = parseUsableLinkIdentifier();
|
||||
|
||||
@@ -508,6 +509,7 @@ export class RecentActivityRule extends Rule {
|
||||
testValue,
|
||||
karmaThreshold,
|
||||
combinedKarma,
|
||||
subredditBreakdown: getSubredditBreakdownByActivityType(activities)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1664,13 +1664,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"description": "Entity to send message to.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit",
|
||||
"description": "Entity to send message to. It can be templated.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit\n\n**Tip:** To send a message to the subreddit of the Activity us `to: 'r/{{item.subreddit}}'`",
|
||||
"examples": [
|
||||
"aUserName",
|
||||
"u/aUserName",
|
||||
"r/aSubreddit"
|
||||
"r/aSubreddit",
|
||||
"r/{{item.subreddit}}"
|
||||
],
|
||||
"pattern": "^\\s*(\\/[ru]\\/|[ru]\\/)*(\\w+)*\\s*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -2445,8 +2445,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"targets": {
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of this Submission.\n\nTemplated the same as **content**",
|
||||
|
||||
@@ -3454,13 +3454,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"description": "Entity to send message to.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit",
|
||||
"description": "Entity to send message to. It can be templated.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit\n\n**Tip:** To send a message to the subreddit of the Activity us `to: 'r/{{item.subreddit}}'`",
|
||||
"examples": [
|
||||
"aUserName",
|
||||
"u/aUserName",
|
||||
"r/aSubreddit"
|
||||
"r/aSubreddit",
|
||||
"r/{{item.subreddit}}"
|
||||
],
|
||||
"pattern": "^\\s*(\\/[ru]\\/|[ru]\\/)*(\\w+)*\\s*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -5814,8 +5814,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"targets": {
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of this Submission.\n\nTemplated the same as **content**",
|
||||
|
||||
@@ -3049,13 +3049,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"description": "Entity to send message to.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit",
|
||||
"description": "Entity to send message to. It can be templated.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit\n\n**Tip:** To send a message to the subreddit of the Activity us `to: 'r/{{item.subreddit}}'`",
|
||||
"examples": [
|
||||
"aUserName",
|
||||
"u/aUserName",
|
||||
"r/aSubreddit"
|
||||
"r/aSubreddit",
|
||||
"r/{{item.subreddit}}"
|
||||
],
|
||||
"pattern": "^\\s*(\\/[ru]\\/|[ru]\\/)*(\\w+)*\\s*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -5139,8 +5139,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"targets": {
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of this Submission.\n\nTemplated the same as **content**",
|
||||
|
||||
@@ -3165,13 +3165,13 @@
|
||||
"type": "string"
|
||||
},
|
||||
"to": {
|
||||
"description": "Entity to send message to.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit",
|
||||
"description": "Entity to send message to. It can be templated.\n\nIf not present Message be will sent to the Author of the Activity being checked.\n\nValid formats:\n\n* `aUserName` -- send to /u/aUserName\n* `u/aUserName` -- send to /u/aUserName\n* `r/aSubreddit` -- sent to modmail of /r/aSubreddit\n\n**Note:** Reddit does not support sending a message AS a subreddit TO another subreddit\n\n**Tip:** To send a message to the subreddit of the Activity us `to: 'r/{{item.subreddit}}'`",
|
||||
"examples": [
|
||||
"aUserName",
|
||||
"u/aUserName",
|
||||
"r/aSubreddit"
|
||||
"r/aSubreddit",
|
||||
"r/{{item.subreddit}}"
|
||||
],
|
||||
"pattern": "^\\s*(\\/[ru]\\/|[ru]\\/)*(\\w+)*\\s*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -5385,8 +5385,18 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"targets": {
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Specify where this Submission should be made\n\nValid values: 'self' | [subreddit]\n\n* 'self' -- DEFAULT. Post Submission to same subreddit of Activity being processed\n* [subreddit] -- The name of a subreddit to post Submission to. EX mealtimevideos"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of this Submission.\n\nTemplated the same as **content**",
|
||||
|
||||
@@ -3,7 +3,7 @@ import objectHash from 'object-hash';
|
||||
import {
|
||||
activityIsDeleted, activityIsFiltered,
|
||||
activityIsRemoved,
|
||||
AuthorTypedActivitiesOptions, BOT_LINK,
|
||||
AuthorTypedActivitiesOptions, BOT_LINK, TemplateContext,
|
||||
getAuthorHistoryAPIOptions, renderContent
|
||||
} from "../Utils/SnoowrapUtils";
|
||||
import {map as mapAsync} from 'async';
|
||||
@@ -161,6 +161,7 @@ import {IncludesData} from "../Common/Infrastructure/Includes";
|
||||
import {parseFromJsonOrYamlToObject} from "../Common/Config/ConfigUtil";
|
||||
import ConfigParseError from "../Utils/ConfigParseError";
|
||||
import {ActivityReport} from "../Common/Entities/ActivityReport";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export const DEFAULT_FOOTER = '\r\n*****\r\nThis action was performed by [a bot.]({{botLink}}) Mention a moderator or [send a modmail]({{modmailLink}}) if you any ideas, questions, or concerns about this action.';
|
||||
|
||||
@@ -1769,9 +1770,24 @@ export class SubredditResources {
|
||||
/**
|
||||
* Convenience method for using getContent and SnoowrapUtils@renderContent in one method
|
||||
* */
|
||||
async renderContent(contentStr: string, data: SnoowrapActivity, ruleResults: RuleResultEntity[] = [], usernotes?: UserNotes) {
|
||||
async renderContent(contentStr: string, activity: SnoowrapActivity, ruleResults: RuleResultEntity[] = [], actionResults: ActionResultEntity[] = [], templateData: TemplateContext = {}) {
|
||||
const content = await this.getContent(contentStr);
|
||||
return await renderContent(content, data, ruleResults, usernotes ?? this.userNotes);
|
||||
|
||||
const {usernotes = this.userNotes, ...restData} = templateData;
|
||||
return await renderContent(content, {
|
||||
...restData,
|
||||
activity,
|
||||
usernotes,
|
||||
ruleResults,
|
||||
actionResults,
|
||||
});
|
||||
}
|
||||
|
||||
async renderFooter(item: Submission | Comment, footer: false | string | undefined = this.footer) {
|
||||
if (footer === false) {
|
||||
return '';
|
||||
}
|
||||
return this.renderContent(footer, item);
|
||||
}
|
||||
|
||||
async getConfigFragment<T>(includesData: IncludesData, validateFunc?: ConfigFragmentValidationFunc): Promise<T> {
|
||||
@@ -3345,19 +3361,6 @@ export class SubredditResources {
|
||||
this.logger.debug(`Cached check result '${result.check.name}' for User ${userName} on Submission ${item.link_id} for ${ttl} seconds (Hash ${hash})`);
|
||||
}
|
||||
|
||||
async generateFooter(item: Submission | Comment, actionFooter?: false | string) {
|
||||
let footer = actionFooter !== undefined ? actionFooter : this.footer;
|
||||
if (footer === false) {
|
||||
return '';
|
||||
}
|
||||
const subName = await item.subreddit.display_name;
|
||||
const permaLink = `https://reddit.com${await item.permalink}`
|
||||
const modmailLink = `https://www.reddit.com/message/compose?to=%2Fr%2F${subName}&message=${encodeURIComponent(permaLink)}`
|
||||
|
||||
const footerRawContent = await this.getContent(footer, item.subreddit);
|
||||
return he.decode(Mustache.render(footerRawContent, {subName, permaLink, modmailLink, botLink: BOT_LINK}));
|
||||
}
|
||||
|
||||
async getImageHash(img: ImageData): Promise<Required<ImageHashCacheData>|undefined> {
|
||||
|
||||
if(img.hashResult !== undefined && img.hashResultFlipped !== undefined) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
asStrongSubredditState,
|
||||
asSubmission,
|
||||
convertSubredditsRawToStrong,
|
||||
formatNumber,
|
||||
getActivityAuthorName,
|
||||
getActivitySubredditName,
|
||||
isStrongSubredditState, isSubmission,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
normalizeName,
|
||||
parseDurationValToDuration,
|
||||
parseRedditEntity,
|
||||
parseRuleResultsToMarkdownSummary, removeUndefinedKeys,
|
||||
parseResultsToMarkdownSummary, removeUndefinedKeys,
|
||||
subredditStateIsNameOnly,
|
||||
toStrongSubredditState,
|
||||
truncateStringToLength,
|
||||
@@ -34,8 +35,14 @@ import {URL} from "url";
|
||||
import {isStatusError, MaybeSeriousErrorWithCause, SimpleError} from "./Errors";
|
||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||
import {StrongSubredditCriteria, SubredditCriteria} from "../Common/Infrastructure/Filters/FilterCriteria";
|
||||
import {DurationVal} from "../Common/Infrastructure/Atomic";
|
||||
import {DurationVal, GenericContentTemplateData} from "../Common/Infrastructure/Atomic";
|
||||
import {ActivityWindowCriteria} from "../Common/Infrastructure/ActivityWindow";
|
||||
import {
|
||||
SnoowrapActivity,
|
||||
SubredditActivityAbsoluteBreakdown,
|
||||
SubredditActivityBreakdown, SubredditActivityBreakdownByType
|
||||
} from "../Common/Infrastructure/Reddit";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
|
||||
export const BOT_LINK = 'https://www.reddit.com/r/ContextModBot/comments/otz396/introduction_to_contextmodbot';
|
||||
|
||||
@@ -119,73 +126,184 @@ export const isSubreddit = async (subreddit: Subreddit, stateCriteria: Subreddit
|
||||
const renderContentCommentTruncate = truncateStringToLength(50);
|
||||
const shortTitleTruncate = truncateStringToLength(15);
|
||||
|
||||
export const renderContent = async (template: string, data: (Submission | Comment), ruleResults: RuleResultEntity[] = [], usernotes: UserNotes) => {
|
||||
const conditional: any = {};
|
||||
if(data.can_mod_post) {
|
||||
conditional.reports = data.num_reports;
|
||||
conditional.modReports = data.mod_reports.length;
|
||||
conditional.userReports = data.user_reports.length;
|
||||
}
|
||||
if(asSubmission(data)) {
|
||||
conditional.nsfw = data.over_18;
|
||||
conditional.spoiler = data.spoiler;
|
||||
conditional.op = true;
|
||||
conditional.upvoteRatio = `${data.upvote_ratio * 100}%`;
|
||||
} else {
|
||||
conditional.op = data.is_submitter;
|
||||
}
|
||||
const templateData: any = {
|
||||
kind: data instanceof Submission ? 'submission' : 'comment',
|
||||
// @ts-ignore
|
||||
author: getActivityAuthorName(await data.author),
|
||||
votes: data.score,
|
||||
age: dayjs.duration(dayjs().diff(dayjs.unix(data.created))).humanize(),
|
||||
permalink: `https://reddit.com${data.permalink}`,
|
||||
export interface TemplateContext {
|
||||
usernotes?: UserNotes
|
||||
check?: string
|
||||
manager?: string
|
||||
ruleResults?: RuleResultEntity[]
|
||||
actionResults?: ActionResultEntity[]
|
||||
activity?: SnoowrapActivity
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const renderContent = async (template: string, data: TemplateContext = {}) => {
|
||||
const {
|
||||
usernotes,
|
||||
ruleResults,
|
||||
actionResults,
|
||||
activity,
|
||||
...restContext
|
||||
} = data;
|
||||
|
||||
let view: GenericContentTemplateData = {
|
||||
botLink: BOT_LINK,
|
||||
id: data.name,
|
||||
...conditional
|
||||
...restContext
|
||||
};
|
||||
|
||||
if(activity !== undefined) {
|
||||
const conditional: any = {};
|
||||
if (activity.can_mod_post) {
|
||||
conditional.reports = activity.num_reports;
|
||||
conditional.modReports = activity.mod_reports.length;
|
||||
conditional.userReports = activity.user_reports.length;
|
||||
}
|
||||
if (asSubmission(activity)) {
|
||||
conditional.nsfw = activity.over_18;
|
||||
conditional.spoiler = activity.spoiler;
|
||||
conditional.op = true;
|
||||
conditional.upvoteRatio = `${activity.upvote_ratio * 100}%`;
|
||||
} else {
|
||||
conditional.op = activity.is_submitter;
|
||||
}
|
||||
|
||||
const subreddit = activity.subreddit.display_name;
|
||||
const permalink = `https://reddit.com${activity.permalink}`;
|
||||
|
||||
view.modmailLink = `https://www.reddit.com/message/compose?to=%2Fr%2F${subreddit}&message=${encodeURIComponent(permalink)}`;
|
||||
|
||||
const templateData: any = {
|
||||
kind: activity instanceof Submission ? 'submission' : 'comment',
|
||||
// @ts-ignore
|
||||
author: getActivityAuthorName(await activity.author),
|
||||
votes: activity.score,
|
||||
age: dayjs.duration(dayjs().diff(dayjs.unix(activity.created))).humanize(),
|
||||
permalink,
|
||||
id: activity.name,
|
||||
subreddit,
|
||||
...conditional
|
||||
}
|
||||
if (template.includes('{{item.notes') && usernotes !== undefined) {
|
||||
// we need to get notes
|
||||
const notesData = await usernotes.getUserNotes(activity.author);
|
||||
// return usable notes data with some stats
|
||||
const current = notesData.length > 0 ? notesData[notesData.length - 1] : undefined;
|
||||
// group by type
|
||||
const grouped = notesData.reduce((acc: any, x) => {
|
||||
const {[x.noteType]: nt = []} = acc;
|
||||
return Object.assign(acc, {[x.noteType]: nt.concat(x)});
|
||||
}, {});
|
||||
templateData.notes = {
|
||||
data: notesData,
|
||||
current,
|
||||
...grouped,
|
||||
};
|
||||
}
|
||||
if (activity instanceof Submission) {
|
||||
templateData.url = activity.url;
|
||||
templateData.title = activity.title;
|
||||
templateData.shortTitle = shortTitleTruncate(activity.title);
|
||||
} else {
|
||||
templateData.title = renderContentCommentTruncate(activity.body);
|
||||
templateData.shortTitle = shortTitleTruncate(activity.body);
|
||||
}
|
||||
|
||||
view.item = templateData;
|
||||
}
|
||||
if (template.includes('{{item.notes')) {
|
||||
// we need to get notes
|
||||
const notesData = await usernotes.getUserNotes(data.author);
|
||||
// return usable notes data with some stats
|
||||
const current = notesData.length > 0 ? notesData[notesData.length - 1] : undefined;
|
||||
// group by type
|
||||
const grouped = notesData.reduce((acc: any, x) => {
|
||||
const {[x.noteType]: nt = []} = acc;
|
||||
return Object.assign(acc, {[x.noteType]: nt.concat(x)});
|
||||
}, {});
|
||||
templateData.notes = {
|
||||
data: notesData,
|
||||
current,
|
||||
...grouped,
|
||||
|
||||
|
||||
if(ruleResults !== undefined) {
|
||||
view = {
|
||||
...view,
|
||||
...parseRuleResultForTemplate(ruleResults)
|
||||
}
|
||||
}
|
||||
|
||||
if(actionResults !== undefined) {
|
||||
view = {
|
||||
...view,
|
||||
...parseActionResultForTemplate(actionResults)
|
||||
}
|
||||
}
|
||||
|
||||
const rendered = Mustache.render(template, view) as string;
|
||||
return he.decode(rendered);
|
||||
}
|
||||
|
||||
export const parseActionResultForTemplate = (actionResults: ActionResultEntity[] = []) => {
|
||||
// normalize rule names and map context data
|
||||
// NOTE: we are relying on users to use unique names for action. If they don't only the last action run of kind X will have its results here
|
||||
const normalizedActionResults = actionResults.reduce((acc: object, actionResult) => {
|
||||
const {
|
||||
success,
|
||||
data:{
|
||||
...restData
|
||||
} = {},
|
||||
result,
|
||||
} = actionResult;
|
||||
let name = actionResult.premise.name;
|
||||
const kind = actionResult.premise.kind.name;
|
||||
if(name === undefined || name === null) {
|
||||
name = kind;
|
||||
}
|
||||
let formattedData: any = {};
|
||||
// remove all non-alphanumeric characters (spaces, dashes, underscore) and set to lowercase
|
||||
// we will set this as the rule property name to make it easy to access results from mustache template
|
||||
const normalName = normalizeName(name);
|
||||
return {
|
||||
...acc, [normalName]: {
|
||||
kind,
|
||||
success,
|
||||
result,
|
||||
...restData,
|
||||
...formattedData,
|
||||
}
|
||||
};
|
||||
}
|
||||
if (data instanceof Submission) {
|
||||
templateData.url = data.url;
|
||||
templateData.title = data.title;
|
||||
templateData.shortTitle = shortTitleTruncate(data.title);
|
||||
} else {
|
||||
templateData.title = renderContentCommentTruncate(data.body);
|
||||
templateData.shortTitle = shortTitleTruncate(data.body);
|
||||
}
|
||||
}, {});
|
||||
|
||||
return {
|
||||
actionSummary: parseResultsToMarkdownSummary(actionResults),
|
||||
actions: normalizedActionResults
|
||||
};
|
||||
}
|
||||
|
||||
export const parseRuleResultForTemplate = (ruleResults: RuleResultEntity[] = []) => {
|
||||
|
||||
// normalize rule names and map context data
|
||||
// NOTE: we are relying on users to use unique names for rules. If they don't only the last rule run of kind X will have its results here
|
||||
const normalizedRuleResults = ruleResults.reduce((acc: object, ruleResult) => {
|
||||
const {
|
||||
//name,
|
||||
triggered,
|
||||
data = {},
|
||||
data:{
|
||||
subredditBreakdown,
|
||||
...restData
|
||||
} = {},
|
||||
result,
|
||||
// premise: {
|
||||
// kind
|
||||
// }
|
||||
} = ruleResult;
|
||||
let name = ruleResult.premise.name;
|
||||
const kind = ruleResult.premise.kind.name;
|
||||
if(name === undefined || name === null) {
|
||||
name = kind;
|
||||
}
|
||||
let formattedData: any = {};
|
||||
if (subredditBreakdown !== undefined) {
|
||||
// format breakdown for markdown
|
||||
if (Array.isArray(subredditBreakdown)) {
|
||||
const bdArr = subredditBreakdown as SubredditActivityBreakdown[];
|
||||
formattedData.subredditBreakdownFormatted = formatSubredditBreakdownAsMarkdownList(bdArr);
|
||||
} else {
|
||||
const bd = subredditBreakdown as SubredditActivityBreakdownByType;
|
||||
|
||||
// default to total
|
||||
formattedData.subredditBreakdownFormatted = formatSubredditBreakdownAsMarkdownList(bd.total);
|
||||
|
||||
const formatted = Object.entries((bd)).reduce((acc: { [key: string]: string }, curr) => {
|
||||
const [name, breakdownData] = curr;
|
||||
acc[`${name}Formatted`] = formatSubredditBreakdownAsMarkdownList(breakdownData);
|
||||
return acc;
|
||||
}, {});
|
||||
formattedData.subredditBreakdown = {...bd, ...formatted};
|
||||
}
|
||||
}
|
||||
// remove all non-alphanumeric characters (spaces, dashes, underscore) and set to lowercase
|
||||
// we will set this as the rule property name to make it easy to access results from mustache template
|
||||
const normalName = normalizeName(name);
|
||||
@@ -194,14 +312,16 @@ export const renderContent = async (template: string, data: (Submission | Commen
|
||||
kind,
|
||||
triggered,
|
||||
result,
|
||||
...data,
|
||||
...restData,
|
||||
...formattedData,
|
||||
}
|
||||
};
|
||||
}, {});
|
||||
|
||||
const view = {item: templateData, ruleSummary: parseRuleResultsToMarkdownSummary(ruleResults), rules: normalizedRuleResults};
|
||||
const rendered = Mustache.render(template, view) as string;
|
||||
return he.decode(rendered);
|
||||
return {
|
||||
ruleSummary: parseResultsToMarkdownSummary(ruleResults),
|
||||
rules: normalizedRuleResults
|
||||
};
|
||||
}
|
||||
|
||||
export interface ItemContent {
|
||||
@@ -391,3 +511,58 @@ export const getAuthorHistoryAPIOptions = (val: any) => {
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export const getSubredditBreakdown = (activities: SnoowrapActivity[] = []): SubredditActivityBreakdown[] => {
|
||||
if(activities.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const total = activities.length;
|
||||
|
||||
const countBd = activities.reduce((acc: { [key: string]: number }, curr) => {
|
||||
const subName = curr.subreddit.display_name;
|
||||
if (acc[subName] === undefined) {
|
||||
acc[subName] = 0;
|
||||
}
|
||||
acc[subName]++;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const breakdown: SubredditActivityBreakdown[] = Object.entries(countBd).reduce((acc, curr) => {
|
||||
const [name, count] = curr;
|
||||
return acc.concat(
|
||||
{
|
||||
name,
|
||||
count,
|
||||
percent: Number.parseFloat(formatNumber((count / total) * 100))
|
||||
}
|
||||
);
|
||||
}, ([] as SubredditActivityBreakdown[]));
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
export const getSubredditBreakdownByActivityType = (activities: SnoowrapActivity[]): SubredditActivityBreakdownByType => {
|
||||
|
||||
return {
|
||||
total: getSubredditBreakdown(activities),
|
||||
submission: getSubredditBreakdown(activities.filter(x => x instanceof Submission)),
|
||||
comment: getSubredditBreakdown(activities.filter(x => x instanceof Comment)),
|
||||
}
|
||||
}
|
||||
|
||||
export const formatSubredditBreakdownAsMarkdownList = (data: SubredditActivityBreakdown[] = []): string => {
|
||||
if(data.length === 0) {
|
||||
return '';
|
||||
}
|
||||
data.sort((a, b) => b.count - a.count);
|
||||
|
||||
const bd = data.map(x => {
|
||||
const entity = parseRedditEntity(x.name);
|
||||
const prefixedName = entity.type === 'subreddit' ? `r/${entity.name}` : `u/${entity.name}`;
|
||||
return `* ${prefixedName} - ${x.count} (${x.percent}%)`
|
||||
}).join('\n');
|
||||
|
||||
return `${bd}\n`;
|
||||
}
|
||||
|
||||
18
src/util.ts
18
src/util.ts
@@ -117,6 +117,7 @@ import {
|
||||
import {RunnableBaseJson} from "./Common/Infrastructure/Runnable";
|
||||
import Snoowrap from "snoowrap";
|
||||
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
||||
import {ActionResultEntity} from "./Common/Entities/ActionResultEntity";
|
||||
|
||||
|
||||
//import {ResembleSingleCallbackComparisonResult} from "resemblejs";
|
||||
@@ -1934,21 +1935,28 @@ export function findLastIndex<T>(array: Array<T>, predicate: (value: T, index: n
|
||||
return -1;
|
||||
}
|
||||
|
||||
export const parseRuleResultsToMarkdownSummary = (ruleResults: RuleResultEntity[]): string => {
|
||||
export const parseResultsToMarkdownSummary = (ruleResults: (RuleResultEntity | ActionResultEntity)[]): string => {
|
||||
const results = ruleResults.map((y) => {
|
||||
let name = y.premise.name;
|
||||
const kind = y.premise.kind.name;
|
||||
if(name === undefined) {
|
||||
name = kind;
|
||||
}
|
||||
const {triggered, result, ...restY} = y;
|
||||
let runIndicator = null;
|
||||
if(y instanceof RuleResultEntity) {
|
||||
runIndicator = y.triggered;
|
||||
} else {
|
||||
runIndicator = y.success;
|
||||
}
|
||||
const {result, ...restY} = y;
|
||||
|
||||
let t = triggeredIndicator(false);
|
||||
if(triggered === null) {
|
||||
if(runIndicator === null) {
|
||||
t = 'Skipped';
|
||||
} else if(triggered === true) {
|
||||
} else if(runIndicator === true) {
|
||||
t = triggeredIndicator(true);
|
||||
}
|
||||
return `* ${name} - ${t} - ${result || '-'}`;
|
||||
return `* ${name} - ${t}${result !== undefined ? ` - ${result}` : ''}`;
|
||||
});
|
||||
return results.join('\r\n');
|
||||
}
|
||||
|
||||
@@ -103,8 +103,8 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await compareImg.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, compareImg.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, compareImg.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.equal(diffNormal, 0);
|
||||
});
|
||||
@@ -116,8 +116,8 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await compareImg.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, compareImg.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, compareImg.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.isAtMost(diffNormal, 4);
|
||||
});
|
||||
@@ -129,8 +129,8 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await compareImg.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, compareImg.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, compareImg.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.equal(diffNormal, 0);
|
||||
});
|
||||
@@ -142,13 +142,13 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await flipped.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, flipped.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, flipped.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.isAtLeast(diffNormal, 50);
|
||||
|
||||
const distanceFlipped = leven(original.hashResult, flipped.hashResultFlipped);
|
||||
const diffFlipped = (distanceFlipped/original.hashResult.length)*100;
|
||||
const distanceFlipped = leven(original.hashResult as string, flipped.hashResultFlipped as string);
|
||||
const diffFlipped = (distanceFlipped/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.isAtMost(diffFlipped, 4);
|
||||
});
|
||||
@@ -160,8 +160,8 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await compareImg.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, compareImg.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, compareImg.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.isAtMost(diffNormal, 10);
|
||||
});
|
||||
@@ -173,8 +173,8 @@ describe('Hash Comparisons', function () {
|
||||
});
|
||||
await compareImg.hash(32);
|
||||
|
||||
const distanceNormal = leven(original.hashResult, compareImg.hashResult);
|
||||
const diffNormal = (distanceNormal/original.hashResult.length)*100;
|
||||
const distanceNormal = leven(original.hashResult as string, compareImg.hashResult as string);
|
||||
const diffNormal = (distanceNormal/(original.hashResult as string).length)*100;
|
||||
|
||||
assert.isAtLeast(diffNormal, 50);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user