From 7696f3c2ff61e5c7348d26fa214b3b3e866ef00c Mon Sep 17 00:00:00 2001 From: Marcin Macinski Date: Wed, 22 Dec 2021 00:45:59 +0100 Subject: [PATCH 1/2] UserFlairAction added --- src/Action/ActionFactory.ts | 7 ++- src/Action/UserFlairAction.ts | 108 ++++++++++++++++++++++++++++++++ src/Action/index.ts | 11 ++-- src/Check/index.ts | 19 +++--- src/Common/types.ts | 3 +- src/Schema/Action.json | 1 + src/Schema/App.json | 95 ++++++++++++++++++++++++++++ src/Utils/SnoowrapClients.ts | 13 ++++ src/Web/assets/views/helper.ejs | 6 ++ src/Web/assets/views/invite.ejs | 5 ++ 10 files changed, 251 insertions(+), 17 deletions(-) create mode 100644 src/Action/UserFlairAction.ts 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..f39c687 --- /dev/null +++ b/src/Action/UserFlairAction.ts @@ -0,0 +1,108 @@ +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); + + if (options.text === undefined && options.css === undefined && options.flair_template_id === undefined) { + throw new Error('Must define either text, css or flair_template_id on UserFlairAction'); + } + + this.text = options.text || ''; + this.css = options.css || ''; + this.flair_template_id = 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 !== '') { + flairParts.push(`Flair template ID: ${this.flair_template_id}`) + } else { + if (this.text !== '') { + flairParts.push(`Text: ${this.text}`); + } + if (this.css !== '') { + flairParts.push(`CSS: ${this.css}`); + } + } + + const flairSummary = flairParts.length === 0 ? 'No user flair (unflaired)' : flairParts.join(' | '); + this.logger.verbose(flairSummary); + if (item && item?.author) { + if (!this.dryRun) { + if (this.flair_template_id !== '') { + // @ts-ignore + await this.client.assignUserFlairByTemplateId({ + subredditName: item.subreddit.display_name, + flairTemplateId: this.flair_template_id, + username: item.author.name, + }) + .then((e: any) => { + this.logger.verbose(JSON.stringify(e)); + }) + .catch((e: any) => { + this.logger.verbose(JSON.stringify(e)); + }) + } + } else { + // @ts-ignore + await (item.author as RedditUser).assignFlair({ + subredditName: item.subreddit.display_name, + cssClass: this.css, + text: this.text, + }) + } + } else { + this.logger.warn('Cannot flair Comment'); + return { + dryRun, + success: false, + result: 'Cannot flair Comment', + } + } + return { + dryRun, + success: true, + result: flairSummary, + } + } +} + +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 it to pick + * */ + flair_template_id?: string; +} + +export interface UserFlairActionOptions extends UserFlairActionConfig, ActionOptions { + +} + +/** + * Flair the Submission + * */ +export interface UserFlairActionJson extends UserFlairActionConfig, ActionJson { + kind: 'userflair' +} \ No newline at end of file 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 cd48860..9fc2ebf 100644 --- a/src/Schema/Action.json +++ b/src/Schema/Action.json @@ -387,6 +387,7 @@ "ban", "comment", "flair", + "userflair", "lock", "message", "remove", diff --git a/src/Schema/App.json b/src/Schema/App.json index 3bcedba..6d64eb9 100644 --- a/src/Schema/App.json +++ b/src/Schema/App.json @@ -1118,6 +1118,9 @@ { "$ref": "#/definitions/FlairActionJson" }, + { + "$ref": "#/definitions/UserFlairActionJson" + }, { "$ref": "#/definitions/CommentActionJson" }, @@ -1422,6 +1425,95 @@ }, "type": "object" }, + "UserFlairActionJson": { + "description": "Flair User", + "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 ID to apply", + "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" + }, "FlairActionJson": { "description": "Flair the Submission", "properties": { @@ -3189,6 +3281,9 @@ ], "items": { "anyOf": [ + { + "$ref": "#/definitions/UserFlairActionJson" + }, { "$ref": "#/definitions/FlairActionJson" }, diff --git a/src/Utils/SnoowrapClients.ts b/src/Utils/SnoowrapClients.ts index 1c79705..4e0f49e 100644 --- a/src/Utils/SnoowrapClients.ts +++ b/src/Utils/SnoowrapClients.ts @@ -33,6 +33,19 @@ export class ExtendedSnoowrap extends Snoowrap { return await this.oauthRequest({uri: '/api/info', method: 'get', qs: { sr_name: names.join(',')}}) as Listing; } + + + async assignUserFlairByTemplateId(options: { flairTemplateId: string, username: string, subredditName: string }): Promise { + return await this.oauthRequest({ + uri: `/r/${options.subredditName}/api/selectflair`, + method: 'post', + form: { + api_type: 'json', + name: options.username, + flair_template_id: options.flairTemplateId, + } + }); + } } export class RequestTrackingSnoowrap extends ExtendedSnoowrap { diff --git a/src/Web/assets/views/helper.ejs b/src/Web/assets/views/helper.ejs index f3fc40c..5a63c10 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 0b94c2a..9299518 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
+
+ + +