diff --git a/src/Action/ActionFactory.ts b/src/Action/ActionFactory.ts index 47bdd24..88d8044 100644 --- a/src/Action/ActionFactory.ts +++ b/src/Action/ActionFactory.ts @@ -10,10 +10,11 @@ import ApproveAction, {ApproveActionConfig} from "./ApproveAction"; import BanAction, {BanActionJson} from "./BanAction"; import {MessageAction, MessageActionJson} from "./MessageAction"; import {SubredditResources} from "../Subreddit/SubredditResources"; -import Snoowrap from "snoowrap"; +import {UserFlairAction, UserFlairActionJson} from './UserFlairAction'; +import {ExtendedSnoowrap} from '../Utils/SnoowrapClients'; export function actionFactory -(config: ActionJson, logger: Logger, subredditName: string, resources: SubredditResources, client: Snoowrap): Action { +(config: ActionJson, logger: Logger, subredditName: string, resources: SubredditResources, client: ExtendedSnoowrap): Action { switch (config.kind) { case 'comment': return new CommentAction({...config as CommentActionJson, logger, subredditName, resources, client}); @@ -25,6 +26,8 @@ export function actionFactory return new ReportAction({...config as ReportActionJson, logger, subredditName, resources, client}); case 'flair': return new FlairAction({...config as FlairActionJson, logger, subredditName, resources, client}); + case 'userflair': + return new UserFlairAction({...config as UserFlairActionJson, logger, subredditName, resources, client}); case 'approve': return new ApproveAction({...config as ApproveActionConfig, logger, subredditName, resources, client}); case 'usernote': diff --git a/src/Action/UserFlairAction.ts b/src/Action/UserFlairAction.ts new file mode 100644 index 0000000..b14880e --- /dev/null +++ b/src/Action/UserFlairAction.ts @@ -0,0 +1,109 @@ +import Action, {ActionConfig, ActionJson, ActionOptions} from './index'; +import {Comment, RedditUser, Submission} from 'snoowrap'; +import {RuleResult} from '../Rule'; +import {ActionProcessResult} from '../Common/interfaces'; + +export class UserFlairAction extends Action { + text?: string; + css?: string; + flair_template_id?: string; + + constructor(options: UserFlairActionOptions) { + super(options); + + this.text = options.text === null || options.text === '' ? undefined : options.text; + this.css = options.css === null || options.text === '' ? undefined : options.text; + this.flair_template_id = options.flair_template_id === null || options.flair_template_id === '' ? undefined : options.flair_template_id; + } + + getKind() { + return 'User Flair'; + } + + async process(item: Comment | Submission, ruleResults: RuleResult[], runtimeDryrun?: boolean): Promise { + const dryRun = runtimeDryrun || this.dryRun; + let flairParts = []; + + if (this.flair_template_id !== undefined) { + flairParts.push(`Flair template ID: ${this.flair_template_id}`) + if(this.text !== undefined || this.css !== undefined) { + this.logger.warn('Text/CSS properties will be ignored since a flair template is specified'); + } + } else { + if (this.text !== undefined) { + flairParts.push(`Text: ${this.text}`); + } + if (this.css !== undefined) { + flairParts.push(`CSS: ${this.css}`); + } + } + + const flairSummary = flairParts.length === 0 ? 'Unflair user' : flairParts.join(' | '); + this.logger.verbose(flairSummary); + + if (!this.dryRun) { + if (this.flair_template_id !== undefined) { + try { + // @ts-ignore + await this.client.assignUserFlairByTemplateId({ + subredditName: item.subreddit.display_name, + flairTemplateId: this.flair_template_id, + username: item.author.name, + }); + } catch (err: any) { + this.logger.error('Either the flair template ID is incorrect or you do not have permission to access it.'); + throw err; + } + } else if (this.text === undefined && this.css === undefined) { + // @ts-ignore + await item.subreddit.deleteUserFlair(item.author.name); + } else { + // @ts-ignore + await item.author.assignFlair({ + subredditName: item.subreddit.display_name, + cssClass: this.css, + text: this.text, + }); + } + } + + return { + dryRun, + success: true, + result: flairSummary, + } + } +} + +/** + * Flair the Author of an Activity + * + * Leave all properties blank or null to remove a User's existing flair + * */ +export interface UserFlairActionConfig extends ActionConfig { + /** + * The text of the flair to apply + * */ + text?: string, + /** + * The text of the css class of the flair to apply + * */ + css?: string, + /** + * Flair template to pick. + * + * **Note:** If this template is used text/css are ignored + * */ + flair_template_id?: string; +} + +export interface UserFlairActionOptions extends UserFlairActionConfig, ActionOptions { + +} + +/** + * Flair the Submission + * */ +export interface UserFlairActionJson extends UserFlairActionConfig, ActionJson { + kind: 'userflair' +} diff --git a/src/Action/index.ts b/src/Action/index.ts index 37e54c6..ab2ba5a 100644 --- a/src/Action/index.ts +++ b/src/Action/index.ts @@ -1,4 +1,4 @@ -import Snoowrap, {Comment, Submission} from "snoowrap"; +import {Comment, Submission} from "snoowrap"; import {Logger} from "winston"; import {RuleResult} from "../Rule"; import {SubredditResources} from "../Subreddit/SubredditResources"; @@ -6,12 +6,13 @@ import {ActionProcessResult, ActionResult, ChecksActivityState, TypedActivitySta import Author, {AuthorOptions} from "../Author/Author"; import {mergeArr} from "../util"; import LoggedError from "../Utils/LoggedError"; +import {ExtendedSnoowrap} from '../Utils/SnoowrapClients'; export abstract class Action { name?: string; logger: Logger; resources: SubredditResources; - client: Snoowrap + client: ExtendedSnoowrap; authorIs: AuthorOptions; itemIs: TypedActivityStates; dryRun: boolean; @@ -114,8 +115,8 @@ export abstract class Action { export interface ActionOptions extends ActionConfig { logger: Logger; subredditName: string; - resources: SubredditResources - client: Snoowrap + resources: SubredditResources; + client: ExtendedSnoowrap; } export interface ActionConfig extends ChecksActivityState { @@ -162,7 +163,7 @@ export interface ActionJson extends ActionConfig { /** * The type of action that will be performed */ - kind: 'comment' | 'lock' | 'remove' | 'report' | 'approve' | 'ban' | 'flair' | 'usernote' | 'message' + kind: 'comment' | 'lock' | 'remove' | 'report' | 'approve' | 'ban' | 'flair' | 'usernote' | 'message' | 'userflair' } export const isActionJson = (obj: object): obj is ActionJson => { diff --git a/src/Check/index.ts b/src/Check/index.ts index 5684e04..9cafd03 100644 --- a/src/Check/index.ts +++ b/src/Check/index.ts @@ -29,7 +29,8 @@ import * as RuleSetSchema from '../Schema/RuleSet.json'; import * as ActionSchema from '../Schema/Action.json'; import {ActionObjectJson, RuleJson, RuleObjectJson, ActionJson as ActionTypeJson} from "../Common/types"; import {SubredditResources} from "../Subreddit/SubredditResources"; -import {Author, AuthorCriteria, AuthorOptions} from "../Author/Author"; +import {Author, AuthorCriteria, AuthorOptions} from '..'; +import {ExtendedSnoowrap} from '../Utils/SnoowrapClients'; const checkLogName = truncateStringToLength(25); @@ -50,7 +51,7 @@ export abstract class Check implements ICheck { dryRun?: boolean; notifyOnTrigger: boolean; resources: SubredditResources; - client: Snoowrap; + client: ExtendedSnoowrap; constructor(options: CheckOptions) { const { @@ -345,13 +346,13 @@ export interface ICheck extends JoinCondition, ChecksActivityState { } export interface CheckOptions extends ICheck { - rules: Array - actions: ActionConfig[] - logger: Logger - subredditName: string - notifyOnTrigger?: boolean - resources: SubredditResources - client: Snoowrap + rules: Array; + actions: ActionConfig[]; + logger: Logger; + subredditName: string; + notifyOnTrigger?: boolean; + resources: SubredditResources; + client: ExtendedSnoowrap; cacheUserResult?: UserResultCacheOptions; } diff --git a/src/Common/types.ts b/src/Common/types.ts index b544ec1..8ad2d8e 100644 --- a/src/Common/types.ts +++ b/src/Common/types.ts @@ -3,6 +3,7 @@ import {RepeatActivityJSONConfig} from "../Rule/RepeatActivityRule"; import {AuthorRuleJSONConfig} from "../Rule/AuthorRule"; import {AttributionJSONConfig} from "../Rule/AttributionRule"; import {FlairActionJson} from "../Action/SubmissionAction/FlairAction"; +import {UserFlairActionJson} from "../Action/UserFlairAction"; import {CommentActionJson} from "../Action/CommentAction"; import {ReportActionJson} from "../Action/ReportAction"; import {LockActionJson} from "../Action/LockAction"; @@ -18,7 +19,7 @@ import {RepostRuleJSONConfig} from "../Rule/RepostRule"; export type RuleJson = RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig | HistoryJSONConfig | RegexRuleJSONConfig | RepostRuleJSONConfig | string; export type RuleObjectJson = Exclude -export type ActionJson = CommentActionJson | FlairActionJson | ReportActionJson | LockActionJson | RemoveActionJson | ApproveActionJson | BanActionJson | UserNoteActionJson | MessageActionJson | string; +export type ActionJson = CommentActionJson | FlairActionJson | ReportActionJson | LockActionJson | RemoveActionJson | ApproveActionJson | BanActionJson | UserNoteActionJson | MessageActionJson | UserFlairActionJson | string; export type ActionObjectJson = Exclude; // borrowed from https://github.com/jabacchetta/set-random-interval/blob/master/src/index.ts diff --git a/src/Schema/Action.json b/src/Schema/Action.json index a2d5827..612849c 100644 --- a/src/Schema/Action.json +++ b/src/Schema/Action.json @@ -391,6 +391,7 @@ "message", "remove", "report", + "userflair", "usernote" ], "type": "string" diff --git a/src/Schema/App.json b/src/Schema/App.json index 341dfab..f20fc5e 100644 --- a/src/Schema/App.json +++ b/src/Schema/App.json @@ -1129,6 +1129,9 @@ { "$ref": "#/definitions/FlairActionJson" }, + { + "$ref": "#/definitions/UserFlairActionJson" + }, { "$ref": "#/definitions/CommentActionJson" }, @@ -3207,6 +3210,9 @@ { "$ref": "#/definitions/FlairActionJson" }, + { + "$ref": "#/definitions/UserFlairActionJson" + }, { "$ref": "#/definitions/CommentActionJson" }, @@ -3508,6 +3514,95 @@ ], "type": "string" }, + "UserFlairActionJson": { + "description": "Flair the Submission", + "properties": { + "authorIs": { + "$ref": "#/definitions/AuthorOptions", + "description": "If present then these Author criteria are checked before running the Action. If criteria fails then the Action is not run.", + "examples": [ + { + "include": [ + { + "flairText": [ + "Contributor", + "Veteran" + ] + }, + { + "isMod": true + } + ] + } + ] + }, + "css": { + "description": "The text of the css class of the flair to apply", + "type": "string" + }, + "dryRun": { + "default": false, + "description": "If `true` the Action will not make the API request to Reddit to perform its action.", + "examples": [ + false, + true + ], + "type": "boolean" + }, + "enable": { + "default": true, + "description": "If set to `false` the Action will not be run", + "examples": [ + true + ], + "type": "boolean" + }, + "flair_template_id": { + "description": "Flair template to pick.\n\n**Note:** If this template is used text/css are ignored", + "type": "string" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/SubmissionState" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/definitions/CommentState" + }, + "type": "array" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + }, + "kind": { + "description": "The type of action that will be performed", + "enum": [ + "userflair" + ], + "type": "string" + }, + "name": { + "description": "An optional, but highly recommended, friendly name for this Action. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes", + "examples": [ + "myDescriptiveAction" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + }, + "text": { + "description": "The text of the flair to apply", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "UserNoteActionJson": { "description": "Add a Toolbox User Note to the Author of this Activity", "properties": { diff --git a/src/Web/assets/views/helper.ejs b/src/Web/assets/views/helper.ejs index dd8cdfe..bda9380 100644 --- a/src/Web/assets/views/helper.ejs +++ b/src/Web/assets/views/helper.ejs @@ -128,6 +128,12 @@ class="font-mono font-semibold">modcontributors for the bot to ban/mute users in the subreddits it moderates +
+ + +
diff --git a/src/Web/assets/views/invite.ejs b/src/Web/assets/views/invite.ejs index 86bd2cd..e86548d 100644 --- a/src/Web/assets/views/invite.ejs +++ b/src/Web/assets/views/invite.ejs @@ -86,6 +86,11 @@ class="font-mono font-semibold">modcontributors for the bot to ban/mute users in the subreddits it moderates
+
+ + +