Compare commits

...

57 Commits

Author SHA1 Message Date
FoxxMD
859680dca8 Merge branch 'edge' 2022-09-01 09:03:27 -04:00
FoxxMD
18c1ac0fd7 Chore: Bump version 2022-09-01 09:03:12 -04:00
FoxxMD
2fb503f09f feat(message): Implement templating for to field so subreddit can be used generically
since 'to' be now be templated a user can configure a message to send to the subreddit the action is being processed from using `to: 'r/{{item.subreddit}}'`
2022-08-31 09:58:24 -04:00
FoxxMD
7e5eeb71da fix(submission): Fix targets data type to accept array 2022-08-31 09:31:25 -04:00
FoxxMD
84f2da8b6d feat(action): Implement Action templating data #104
* Add top-level 'actionSummary' template variable that renders a markdown list of action results
* Add individual action result/data, in the same structure as rules, under the top-level 'actions' template variable
2022-08-30 17:12:51 -04:00
FoxxMD
be51f8ae43 fix(image): Fix hash result test assertion 2022-08-30 16:13:35 -04:00
FoxxMD
f82d985eab feat(template): Refactor templating to be more extensible and add some requested data
* Implement interfaces for template parts
* Refactor Action constructor to take an object for runtime options (cleaner, more extensible)
* Refactor subreddit resource and snoowraputils content rendering and organization to use objects of optional data rather than required arguments
  * Make almost all data optional and only parse/render if included
  * Move rule results parsing/formatting into own function
  * Refactor footer render to use renderContent (DRY)
* Add some requested template data #104
  * {{item.subreddit}} #87
  * {{check}} #87
* Updated templating documentation
2022-08-26 13:42:02 -04:00
FoxxMD
aab014650a fix(template): Fix rule data spread to template 2022-08-26 11:22:59 -04:00
FoxxMD
113ac3e10e fix(template): Fix subreddit breakdown reference override 2022-08-25 17:14:28 -04:00
FoxxMD
afb6aad26d fix(template): Fix subreddit breakdown when empty activities array 2022-08-25 16:30:15 -04:00
FoxxMD
33b60825d9 fix(template): Return empty string if no subreddits in breakdown 2022-08-25 16:16:15 -04:00
FoxxMD
b5202a33ac feat(template): Add subreddit breakdown to template data for Recent and History rule 2022-08-25 10:39:03 -04:00
FoxxMD
ffa1e423b2 Merge branch 'edge' 2022-08-23 09:49:23 -04:00
FoxxMD
09cb08492c Merge branch 'edge' 2022-08-23 09:47:59 -04:00
FoxxMD
d9ab81ab8c Merge branch 'edge' 2022-07-27 09:19:30 -04:00
FoxxMD
98691bd19c Merge branch 'edge' 2022-07-15 09:27:22 -04:00
FoxxMD
8123c34463 Merge branch 'edge' 2022-06-21 16:13:54 -04:00
FoxxMD
3292d011fa Merge branch 'edge' 2022-06-21 10:03:14 -04:00
FoxxMD
661a0ae440 Merge branch 'edge' 2022-05-26 09:59:32 -04:00
FoxxMD
05f477b67d Merge branch 'edge' 2022-05-12 12:27:51 -04:00
Matt Foxx
1317a5916c Merge pull request #86 from wchristian/example_fix
trying to use names key in authorfilter causes config parse failure
2022-04-05 16:55:56 -04:00
Christian Walde
e9135ec1ef trying to use names key in authorfilter causes config parse failure 2022-04-05 13:49:41 +02:00
FoxxMD
e58a0f8f21 Merge branch 'edge' 2022-03-14 12:39:05 -04:00
FoxxMD
f7cebc013b Merge branch 'edge' 2022-03-08 09:48:06 -05:00
FoxxMD
ae8e11feb4 Merge branch 'edge' 2022-02-22 11:11:46 -05:00
FoxxMD
e07b8cc291 Merge branch 'edge' 2022-02-18 11:58:28 -05:00
FoxxMD
fc51928054 Merge branch 'edge' 2022-02-02 16:59:56 -05:00
FoxxMD
e2590e50f8 Merge branch 'edge' 2022-01-28 17:27:51 -05:00
FoxxMD
aaed0d3419 Merge branch 'edge' 2022-01-21 10:46:11 -05:00
FoxxMD
bc7eff8928 Merge branch 'edge' 2022-01-14 15:27:09 -05:00
FoxxMD
d6954533a0 Merge branch 'edge' 2022-01-10 12:32:14 -05:00
FoxxMD
ba53233640 Merge branch 'edge' 2022-01-07 09:31:14 -05:00
FoxxMD
1ac7ad4724 Merge branch 'edge' 2022-01-03 16:35:01 -05:00
FoxxMD
2a282a0d6f Merge branch 'edge' 2021-12-21 09:35:21 -05:00
FoxxMD
fd5a92758d Merge branch 'edge' 2021-11-28 19:43:20 -05:00
FoxxMD
39daa11f2d Merge branch 'edge' 2021-11-15 12:53:28 -05:00
FoxxMD
dac6541e28 Merge branch 'edge' 2021-11-01 16:12:43 -04:00
FoxxMD
97906281e6 Merge branch 'edge' 2021-11-01 14:55:10 -04:00
FoxxMD
487f13f704 Merge branch 'edge' 2021-10-12 11:56:51 -04:00
FoxxMD
631e21452c Merge branch 'edge' 2021-09-28 16:36:13 -04:00
FoxxMD
4f3685a1f5 Merge branch 'edge' 2021-09-21 15:18:38 -04:00
FoxxMD
d2d945db2c Merge branch 'edge' 2021-09-21 15:08:28 -04:00
FoxxMD
910f7f79ef Merge branch 'edge' 2021-09-20 10:54:32 -04:00
FoxxMD
a11b667d5e Merge branch 'edge' 2021-09-13 16:16:55 -04:00
FoxxMD
885e3fa765 Merge branch 'edge' 2021-08-26 16:04:01 -04:00
FoxxMD
465c3c9acf Merge branch 'edge' 2021-08-20 15:02:24 -04:00
FoxxMD
161251a943 Merge branch 'edge' 2021-08-05 14:40:06 -04:00
FoxxMD
ce4cb96d9a Merge branch 'edge' 2021-08-03 23:39:14 -04:00
FoxxMD
c317f95953 Merge branch 'edge' 2021-08-03 22:43:02 -04:00
FoxxMD
d0e0515990 Merge branch 'edge' 2021-08-02 15:44:57 -04:00
FoxxMD
cdddd8de48 Merge branch 'edge' 2021-07-30 18:17:38 -04:00
FoxxMD
f598215d88 Merge branch 'edge' 2021-07-30 14:46:51 -04:00
FoxxMD
0c7218571c Merge branch 'edge' 2021-07-29 13:25:16 -04:00
FoxxMD
acc7c49e0e Merge branch 'edge' 2021-07-29 11:27:42 -04:00
FoxxMD
01839512d5 Merge branch 'edge' 2021-07-29 11:14:33 -04:00
FoxxMD
4680640b0c Merge branch 'develop' 2021-07-28 16:58:36 -04:00
Matt Foxx
b813ebdd96 Create dockerhub.yml 2021-07-28 11:27:04 -04:00
38 changed files with 665 additions and 206 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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.');
}

View File

@@ -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 = [];

View File

@@ -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
}
};
}

View File

@@ -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;

View File

@@ -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')
}
};
}

View File

@@ -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();

View File

@@ -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?)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(' | ')
}
}

View File

@@ -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 = [];

View File

@@ -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 {

View File

@@ -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 !== '') {

View File

@@ -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 = [];

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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(' | ')}`);

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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[]
}

View File

@@ -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';

View File

@@ -1730,6 +1730,7 @@ export interface ActionProcessResult {
dryRun: boolean,
result?: string
touchedEntities?: (Submission | Comment | RedditUser | string)[]
data?: any
}
export interface EventActivity {

View File

@@ -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 = [];

View File

@@ -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)
}
};
}

View File

@@ -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**",

View File

@@ -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**",

View File

@@ -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**",

View File

@@ -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**",

View File

@@ -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) {

View File

@@ -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`;
}

View File

@@ -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');
}

View File

@@ -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);
});