├── .nvmrc ├── tests └── assets │ ├── nonImage.txt │ ├── rick-copy.jpg │ ├── rick-ratio.jpg │ ├── rick-border.jpg │ ├── rick-flipped.jpg │ ├── rick-smaller.jpg │ ├── rick-whitebg.jpg │ ├── rick-original.jpg │ └── rick-saturated.jpg ├── .github ├── push-hook-sample.json ├── FUNDING.yml └── workflows │ └── pages.yml ├── act.env.example ├── docs ├── logo.png ├── favicon.ico ├── images │ ├── editor.jpg │ ├── guests.jpg │ ├── logs.png │ ├── oauth.jpg │ ├── grafana.jpg │ ├── runInput.png │ ├── config │ │ ├── save.png │ │ ├── config.jpg │ │ ├── enable.png │ │ ├── errors.png │ │ ├── syntax.png │ │ ├── correctness.png │ │ └── configUpdate.png │ ├── configBox.png │ ├── actionsEvents.png │ ├── botOperations.png │ ├── oauth-invite.jpg │ ├── subredditInvite.jpg │ ├── subredditStatus.jpg │ └── diagram-highlevel.jpg └── subreddit-configuration │ ├── in-depth │ ├── README.md │ ├── regex │ │ ├── matchThresholdCurrentActivity.yaml │ │ ├── matchAnyCurrentActivity.yaml │ │ ├── matchThresholdCurrentActivity.json5 │ │ ├── matchSubmissionParts.yaml │ │ ├── matchAnyCurrentActivity.json5 │ │ ├── matchHistoryActivity.yaml │ │ ├── matchSubmissionParts.json5 │ │ ├── matchHistoryActivity.json5 │ │ ├── matchActivityThresholdHistory.yaml │ │ ├── matchTotalHistoryActivity.yaml │ │ ├── matchActivityThresholdHistory.json5 │ │ ├── matchSubsetHistoryActivity.yaml │ │ ├── matchTotalHistoryActivity.json5 │ │ ├── matchSubsetHistoryActivity.json5 │ │ └── removeDiscordSpam.yaml │ ├── author │ │ ├── flairVettedUserSubmission.yaml │ │ ├── flairNewUserSubmission.yaml │ │ ├── flairVettedUserSubmission.json5 │ │ ├── flairNewUserSubmission.json5 │ │ ├── onlyfansFlair.yaml │ │ └── ignoreVettedUser.yaml │ ├── history │ │ ├── lowEngagement.yaml │ │ ├── opOnlyEngagement.yaml │ │ ├── lowEngagement.json5 │ │ └── opOnlyEngagement.json5 │ ├── userNotes │ │ ├── usernoteSP.yaml │ │ ├── usernoteFilter.yaml │ │ ├── usernoteSP.json5 │ │ ├── usernoteFilter.json5 │ │ └── README.md │ ├── repeatActivity │ │ ├── crosspostSpamming.yaml │ │ ├── burstPosting.yaml │ │ ├── crosspostSpamming.json5 │ │ └── burstPosting.json5 │ ├── recentActivity │ │ ├── freeKarma.yaml │ │ ├── freeKarmaOnSubmission.yaml │ │ ├── freeKarma.json5 │ │ └── freeKarmaOnSubmission.json5 │ └── attribution │ │ ├── redditSelfPromoSubmissionOnly.yaml │ │ ├── redditSelfPromoAll.yaml │ │ ├── redditSelfPromoAll.json5 │ │ └── redditSelfPromoSubmissionsOnly.json5 │ └── cookbook │ ├── commentSpam.yaml │ ├── submissionRepost.yaml │ ├── floodingNewSubmissions.yaml │ ├── sexSolicitationHistory.yaml │ ├── diametricSpam.yaml │ ├── transcribersOfReddit.yaml │ ├── freekarma.yaml │ ├── brigadingNoHistory.yaml │ ├── youtubeCommentRepost.yaml │ ├── newtube.yaml │ └── crosspostSpam.yaml ├── heroku.yml ├── .mocharc.json ├── src ├── Web │ ├── assets │ │ ├── public │ │ │ ├── logo.png │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── yaml │ │ │ │ ├── 59002d8cc9a0e78be5b7.ttf │ │ │ │ ├── entry.js.LICENSE.txt │ │ │ │ ├── 2906.entry.js.LICENSE.txt │ │ │ │ ├── 1569.entry.js │ │ │ │ ├── 7792.entry.js.LICENSE.txt │ │ │ │ ├── 4896.entry.js │ │ │ │ ├── 9741.entry.js │ │ │ │ ├── 8975.entry.js │ │ │ │ ├── 4369.entry.js │ │ │ │ ├── 6330.entry.js │ │ │ │ ├── 9482.entry.js │ │ │ │ ├── 1594.entry.js │ │ │ │ ├── 3553.entry.js │ │ │ │ ├── 1585.entry.js │ │ │ │ ├── 3315.entry.js │ │ │ │ ├── 7135.entry.js │ │ │ │ ├── 6022.entry.js │ │ │ │ └── 6331.entry.js │ │ │ ├── site.webmanifest │ │ │ └── questionsymbol.svg │ │ ├── views │ │ │ ├── close.ejs │ │ │ ├── partials │ │ │ │ ├── logSettingsJs.ejs │ │ │ │ ├── paginationJs.ejs │ │ │ │ ├── loadingIcon.ejs │ │ │ │ ├── instanceTabJs.ejs │ │ │ │ ├── title.ejs │ │ │ │ ├── logSettings.ejs │ │ │ │ ├── head.ejs │ │ │ │ ├── footer.ejs │ │ │ │ └── subredditsTab.ejs │ │ │ ├── error.ejs │ │ │ ├── error-authenticated.ejs │ │ │ └── noAccess.ejs │ │ └── browser.js │ ├── types │ │ └── express │ │ │ └── index.d.ts │ ├── Common │ │ └── User │ │ │ └── CMUser.ts │ └── Server │ │ └── routes │ │ └── authenticated │ │ └── applicationRoutes.ts ├── Utils │ ├── LoggedError.ts │ ├── ConfigParseError.ts │ ├── StringMatching │ │ ├── levenSimilarity.ts │ │ └── CosineSimilarity.ts │ ├── InvalidRegexError.ts │ ├── typeormUtils.ts │ └── AbortToken.ts ├── Action │ ├── SubmissionAction │ │ └── index.ts │ └── LockAction.ts ├── Common │ ├── Entities │ │ ├── EntityRunState │ │ │ ├── EventsRunState.ts │ │ │ ├── QueueRunState.ts │ │ │ ├── ManagerRunState.ts │ │ │ └── EntityRunState.ts │ │ ├── RuleType.ts │ │ ├── ActionType.ts │ │ ├── InvokeeType.ts │ │ ├── RunStateType.ts │ │ ├── AuthorEntity.ts │ │ ├── Base │ │ │ ├── TimeAwareRandomBaseEntity.ts │ │ │ ├── TimeAwareAndUpdatedBaseEntity.ts │ │ │ ├── TimeAwareBaseEntity.ts │ │ │ └── RandomIdBaseEntity.ts │ │ ├── RunnableAssociation │ │ │ ├── CheckToRuleResultEntity.ts │ │ │ ├── RuleSetToRuleResultEntity.ts │ │ │ ├── CheckToRuleSetResultEntity.ts │ │ │ └── RunnableToResultEntity.ts │ │ ├── FilterCriteria │ │ │ ├── AuthorFilterCriteria.ts │ │ │ ├── ActivityStateFilterCriteria.ts │ │ │ ├── ActivityStateFilterCriteriaResult.ts │ │ │ ├── AuthorFilterResult.ts │ │ │ ├── ActivityStateFilterResult.ts │ │ │ ├── FilterResult.ts │ │ │ └── FilterCriteriaResult.ts │ │ ├── Subreddit.ts │ │ ├── Guest │ │ │ └── GuestInterfaces.ts │ │ ├── Transformers │ │ │ └── index.ts │ │ ├── RunEntity.ts │ │ ├── CheckEntity.ts │ │ ├── Stats │ │ │ ├── TotalStat.ts │ │ │ └── TimeSeriesStat.ts │ │ ├── ActivityReport.ts │ │ └── CMEvent.ts │ ├── Infrastructure │ │ ├── Filters │ │ │ └── AuthorCritPropHelper.ts │ │ ├── ActionShapes.ts │ │ ├── RuleShapes.ts │ │ ├── Database.ts │ │ ├── Runnable.ts │ │ └── Includes.ts │ ├── Influx │ │ └── interfaces.ts │ ├── Config │ │ ├── ConfigToObjectOptions.ts │ │ ├── YamlConfigDocument.ts │ │ ├── AbstractConfigDocument.ts │ │ └── JsonConfigDocument.ts │ ├── Migrations │ │ └── Database │ │ │ ├── Server │ │ │ ├── 1663609045418-mhs.ts │ │ │ ├── 1661183583080-submission.ts │ │ │ ├── 1663001719622-subredditInvite.ts │ │ │ ├── 1658930394548-Guests.ts │ │ │ └── 1642180274563-initialData.ts │ │ │ └── Web │ │ │ └── 1660588028346-removeInvites.ts │ ├── WebEntities │ │ ├── ClientSession.ts │ │ └── WebSetting.ts │ ├── ActivitySource.ts │ ├── defaults.ts │ └── Subreddit │ │ └── SubredditResourceInterfaces.ts ├── Rule │ └── SubmissionRule │ │ └── index.ts ├── Check │ └── SubmissionCheck.ts ├── SubredditConfigData.ts ├── Notification │ └── DiscordNotifier.ts └── Subreddit │ └── ModNotes │ ├── ModUserNote.ts │ └── ModAction.ts ├── .idea ├── .gitignore ├── vcs.xml ├── compiler.xml ├── jsLibraryMappings.xml ├── modules.xml └── redditcontextbot.iml ├── ormconfig.json ├── docker ├── root │ └── etc │ │ ├── cont-init.d │ │ └── 10-config │ │ └── services.d │ │ └── node │ │ └── run └── config │ └── docker-compose │ └── config.yaml ├── .dockerignore ├── .nycrc.json ├── ormconfig.js ├── register.js ├── tsconfig.json ├── heroku.Dockerfile ├── Gemfile ├── _config.yml ├── LICENSE ├── patches └── snoowrap+1.23.0.patch └── app.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.14.2 2 | -------------------------------------------------------------------------------- /tests/assets/nonImage.txt: -------------------------------------------------------------------------------- 1 | I am not an image 2 | -------------------------------------------------------------------------------- /.github/push-hook-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/edge" 3 | } 4 | -------------------------------------------------------------------------------- /act.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN= 2 | DOCKERHUB_USERNAME= 3 | DOCKER_PASSWORD= 4 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/logo.png -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/images/editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/editor.jpg -------------------------------------------------------------------------------- /docs/images/guests.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/guests.jpg -------------------------------------------------------------------------------- /docs/images/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/logs.png -------------------------------------------------------------------------------- /docs/images/oauth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/oauth.jpg -------------------------------------------------------------------------------- /docs/images/grafana.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/grafana.jpg -------------------------------------------------------------------------------- /docs/images/runInput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/runInput.png -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | web: heroku.Dockerfile 4 | worker: heroku.Dockerfile 5 | -------------------------------------------------------------------------------- /docs/images/config/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/save.png -------------------------------------------------------------------------------- /docs/images/configBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/configBox.png -------------------------------------------------------------------------------- /tests/assets/rick-copy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-copy.jpg -------------------------------------------------------------------------------- /tests/assets/rick-ratio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-ratio.jpg -------------------------------------------------------------------------------- /docs/images/actionsEvents.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/actionsEvents.png -------------------------------------------------------------------------------- /docs/images/botOperations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/botOperations.png -------------------------------------------------------------------------------- /docs/images/config/config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/config.jpg -------------------------------------------------------------------------------- /docs/images/config/enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/enable.png -------------------------------------------------------------------------------- /docs/images/config/errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/errors.png -------------------------------------------------------------------------------- /docs/images/config/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/syntax.png -------------------------------------------------------------------------------- /docs/images/oauth-invite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/oauth-invite.jpg -------------------------------------------------------------------------------- /tests/assets/rick-border.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-border.jpg -------------------------------------------------------------------------------- /tests/assets/rick-flipped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-flipped.jpg -------------------------------------------------------------------------------- /tests/assets/rick-smaller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-smaller.jpg -------------------------------------------------------------------------------- /tests/assets/rick-whitebg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-whitebg.jpg -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["./register.js", "source-map-support/register"], 3 | "reporter": "dot" 4 | } 5 | -------------------------------------------------------------------------------- /docs/images/subredditInvite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/subredditInvite.jpg -------------------------------------------------------------------------------- /docs/images/subredditStatus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/subredditStatus.jpg -------------------------------------------------------------------------------- /src/Web/assets/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/logo.png -------------------------------------------------------------------------------- /src/Web/assets/views/close.ejs: -------------------------------------------------------------------------------- 1 | 2 | All good! 3 | 4 | 7 | -------------------------------------------------------------------------------- /tests/assets/rick-original.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-original.jpg -------------------------------------------------------------------------------- /tests/assets/rick-saturated.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/tests/assets/rick-saturated.jpg -------------------------------------------------------------------------------- /docs/images/config/correctness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/correctness.png -------------------------------------------------------------------------------- /docs/images/diagram-highlevel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/diagram-highlevel.jpg -------------------------------------------------------------------------------- /src/Web/assets/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/favicon.ico -------------------------------------------------------------------------------- /docs/images/config/configUpdate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/docs/images/config/configUpdate.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [FoxxMD] 2 | patreon: FoxxMD 3 | custom: ["bitcoincash:qqmpsh365r8n9jhp4p8ks7f7qdr7203cws4kmkmr8q"] 4 | -------------------------------------------------------------------------------- /src/Web/assets/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/favicon-16x16.png -------------------------------------------------------------------------------- /src/Web/assets/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/favicon-32x32.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /src/Web/assets/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/Web/assets/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/Web/assets/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/59002d8cc9a0e78be5b7.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoxxMD/context-mod/HEAD/src/Web/assets/public/yaml/59002d8cc9a0e78be5b7.ttf -------------------------------------------------------------------------------- /src/Utils/LoggedError.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from "es6-error"; 2 | 3 | class LoggedError extends ExtendableError { 4 | 5 | } 6 | 7 | export default LoggedError; 8 | -------------------------------------------------------------------------------- /src/Utils/ConfigParseError.ts: -------------------------------------------------------------------------------- 1 | import LoggedError from "./LoggedError"; 2 | 3 | class ConfigParseError extends LoggedError { 4 | 5 | } 6 | 7 | export default ConfigParseError 8 | -------------------------------------------------------------------------------- /src/Web/assets/browser.js: -------------------------------------------------------------------------------- 1 | const logform = require('logform'); 2 | const tripleBeam = require('triple-beam'); 3 | 4 | window.format = logform.format; 5 | window.beam = tripleBeam; 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | parent: Subreddit Configuration 3 | has_children: true 4 | has_toc: true 5 | --- 6 | 7 | # In Depth 8 | 9 | Further details and examples for CM components. 10 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/entry.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/LICENSE */ 2 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchThresholdCurrentActivity.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | - regex: '/fuck|shit|damn/' 5 | # triggers if current activity has greater than 5 matches 6 | matchThreshold: '> 5' 7 | -------------------------------------------------------------------------------- /src/Action/SubmissionAction/index.ts: -------------------------------------------------------------------------------- 1 | import Action, {ActionConfig} from "../index"; 2 | 3 | export abstract class SubmissionAction extends Action { 4 | 5 | } 6 | 7 | export interface SubmissionActionConfig extends ActionConfig { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/Common/Entities/EntityRunState/EventsRunState.ts: -------------------------------------------------------------------------------- 1 | import {EntityRunState} from "./EntityRunState"; 2 | import {ChildEntity} from "typeorm"; 3 | 4 | @ChildEntity() 5 | export class EventsRunState extends EntityRunState { 6 | type = 'events'; 7 | } 8 | -------------------------------------------------------------------------------- /src/Common/Entities/EntityRunState/QueueRunState.ts: -------------------------------------------------------------------------------- 1 | import {EntityRunState} from "./EntityRunState"; 2 | import {ChildEntity} from "typeorm"; 3 | 4 | @ChildEntity() 5 | export class QueueRunState extends EntityRunState { 6 | type = 'queue'; 7 | } 8 | -------------------------------------------------------------------------------- /src/Common/Entities/EntityRunState/ManagerRunState.ts: -------------------------------------------------------------------------------- 1 | import {EntityRunState} from "./EntityRunState"; 2 | import {ChildEntity} from "typeorm"; 3 | 4 | @ChildEntity() 5 | export class ManagerRunState extends EntityRunState { 6 | type = 'manager'; 7 | } 8 | -------------------------------------------------------------------------------- /src/Web/assets/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchAnyCurrentActivity.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | - regex: '/fuck|shit|damn/' 5 | # if "matchThreshold" is not specified it defaults to this -- default behavior is to trigger if there are any matches 6 | #matchThreshold: "> 0" 7 | -------------------------------------------------------------------------------- /src/Rule/SubmissionRule/index.ts: -------------------------------------------------------------------------------- 1 | import {IRule, Rule, RuleJSONConfig} from "../index"; 2 | 3 | export abstract class SubmissionRule extends Rule { 4 | 5 | } 6 | 7 | export interface ISubmissionRule extends IRule { 8 | } 9 | 10 | export interface SubmissionRuleJSONConfig extends RuleJSONConfig { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/2906.entry.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*!--------------------------------------------------------------------------------------------- 2 | * Copyright (C) David Owens II, owensd.io. All rights reserved. 3 | *--------------------------------------------------------------------------------------------*/ 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Common/Infrastructure/Filters/AuthorCritPropHelper.ts: -------------------------------------------------------------------------------- 1 | import {SafeDictionary} from "ts-essentials"; 2 | import {AuthorCriteria} from "./FilterCriteria"; 3 | import {FilterCriteriaPropertyResult} from "./FilterShapes"; 4 | 5 | export type AuthorCritPropHelper = SafeDictionary, keyof AuthorCriteria>; 6 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "sqljs", 3 | "autoSave": true, 4 | "location": "database.sqlite", 5 | "synchronize": true, 6 | "logging": "all", 7 | "entities": [ 8 | "src/Common/Entities/**/*.js" 9 | ], 10 | "migrations": [ 11 | "src/Common/Migrations/Database/**/*.js" 12 | ], 13 | "subscribers": [] 14 | } 15 | -------------------------------------------------------------------------------- /docker/root/etc/cont-init.d/10-config: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # used https://github.com/linuxserver/docker-plex as a template 4 | 5 | # make data folder if not /config 6 | if [ ! -d "${DATA_DIR}" ]; then \ 7 | mkdir -p "${DATA_DIR}" 8 | chown -R abc:abc /config 9 | fi 10 | 11 | # permissions 12 | chown abc:abc \ 13 | /config \ 14 | /config/* 15 | -------------------------------------------------------------------------------- /docker/root/etc/services.d/node/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv bash 2 | 3 | # used https://github.com/linuxserver/docker-plex as a template 4 | 5 | # NODE_ARGS can be passed by ENV in docker command like "docker run foxxmd/context-mod -e NODE_ARGS=--optimize_for_size" 6 | 7 | exec \ 8 | s6-setuidgid abc \ 9 | /usr/local/bin/node $NODE_ARGS /app/src/index.js run 10 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchThresholdCurrentActivity.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | { 8 | "regex": "/fuck|shit|damn/", 9 | // triggers if current activity has greater than 5 matches 10 | "matchThreshold": "> 5" 11 | }, 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/logSettingsJs.ejs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/Common/Entities/RuleType.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"; 2 | 3 | @Entity() 4 | export class RuleType { 5 | 6 | @PrimaryGeneratedColumn() 7 | id!: number; 8 | 9 | @Column("varchar", {length: 200}) 10 | name!: string; 11 | 12 | constructor(name?: string) { 13 | if(name !== undefined) { 14 | this.name = name; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | logs 3 | .github 4 | _site 5 | .bundle 6 | vendor 7 | docs/.jekyll-cache 8 | node_modules 9 | coverage 10 | .nyc_output 11 | .idea 12 | *.bak 13 | *.sqlite 14 | *.sqlite* 15 | *.json 16 | *.json5 17 | *.yaml 18 | *.yml 19 | *.env 20 | 21 | # exceptions 22 | !heroku.yml 23 | !app.json 24 | !.nycrc.json 25 | !.mocharc.json 26 | !tsconfig.json 27 | !package*.json 28 | !docker/config/** 29 | !_config.yml 30 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchSubmissionParts.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | - regex: '/fuck|shit|damn/' 5 | # triggers if the current activity has more than 0 matches 6 | # if the activity is a submission then matches against title, body, and url 7 | # if "testOn" is not provided then `title, body` are the defaults 8 | testOn: 9 | - title 10 | - body 11 | - url 12 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchAnyCurrentActivity.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | // triggers if current activity has more than 0 matches 8 | { 9 | "regex": "/fuck|shit|damn/", 10 | // if "matchThreshold" is not specified it defaults to this -- default behavior is to trigger if there are any matches 11 | // "matchThreshold": "> 0" 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/Common/Influx/interfaces.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDB, WriteApi, WriteOptions} from "@influxdata/influxdb-client/dist"; 2 | 3 | export interface InfluxConfig { 4 | credentials: InfluxCredentials 5 | defaultTags?: Record 6 | writeOptions?: WriteOptions 7 | useKeepAliveAgent?: boolean 8 | debug?: boolean 9 | } 10 | 11 | export interface InfluxCredentials { 12 | url: string 13 | token: string 14 | org: string 15 | bucket: string 16 | } 17 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchHistoryActivity.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | # triggers if any activity in the last 10 (including current activity) match the regex 5 | - regex: '/fuck|shit|damn/' 6 | # if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 7 | # learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 8 | window: 10 9 | -------------------------------------------------------------------------------- /src/Common/Entities/ActionType.ts: -------------------------------------------------------------------------------- 1 | import {Entity, PrimaryColumn, Column, PrimaryGeneratedColumn} from "typeorm"; 2 | import {ActionTypes} from "../Infrastructure/Atomic"; 3 | 4 | @Entity() 5 | export class ActionType { 6 | 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column("varchar", {length: 200}) 11 | name!: string 12 | 13 | constructor(name?: string) { 14 | if(name !== undefined) { 15 | this.name = name; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Common/Infrastructure/ActionShapes.ts: -------------------------------------------------------------------------------- 1 | import {StructuredRunnableBase} from "./Runnable"; 2 | import {ActionJson} from "../types"; 3 | import {IncludesData} from "./Includes"; 4 | 5 | export type ActionConfigData = ActionJson; 6 | export type ActionConfigHydratedData = Exclude; 7 | export type ActionConfigObject = Exclude; 8 | export type StructuredActionObjectJson = Omit & StructuredRunnableBase 9 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "exclude": [ 4 | "node_modules/", 5 | "**/src/Schema/**", 6 | "**/src/Web/assets/**", 7 | "**/tests/**", 8 | "register.js", 9 | "**/src/**/*.d.ts" 10 | ], 11 | "include": [ 12 | "**/src/**/*.ts", 13 | "**/src/**/*.js", 14 | "**/src/**/*.js.map" 15 | ], 16 | "extension": [ 17 | ".ts" 18 | ], 19 | "reporter": [ 20 | "text-summary", 21 | "html" 22 | ], 23 | "report-dir": "./coverage" 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/Config/ConfigToObjectOptions.ts: -------------------------------------------------------------------------------- 1 | import AbstractConfigDocument from "./AbstractConfigDocument"; 2 | import {OperatorJsonConfig} from "../interfaces"; 3 | import {Document as YamlDocument} from "yaml"; 4 | 5 | export interface ConfigToObjectOptions { 6 | location?: string, 7 | jsonDocFunc?: (content: string, location?: string) => AbstractConfigDocument, 8 | yamlDocFunc?: (content: string, location?: string) => AbstractConfigDocument 9 | allowArrays?: boolean 10 | } 11 | -------------------------------------------------------------------------------- /ormconfig.js: -------------------------------------------------------------------------------- 1 | const {DataSource} = require("typeorm"); 2 | const {CMNamingStrategy} = require("./src/Utils/CMNamingStrategy"); 3 | 4 | const MyDataSource = new DataSource({ 5 | type: "sqljs", 6 | autoSave: true, 7 | location: "database.sqlite", 8 | logging: "all", 9 | entities: [ 10 | "src/Common/Entities/**/*.js" 11 | ], 12 | migrations: [ 13 | "src/Common/Migrations/Database/**/*.js" 14 | ], 15 | namingStrategy: new CMNamingStrategy(), 16 | }) 17 | 18 | exports.sqljs = MyDataSource; 19 | -------------------------------------------------------------------------------- /src/Common/Migrations/Database/Server/1663609045418-mhs.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm" 2 | import {RuleType} from "../../../Entities/RuleType"; 3 | 4 | export class mhs1663609045418 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.manager.getRepository(RuleType).save([ 8 | new RuleType('mhs'), 9 | ]); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Overrides the tsconfig used for the app. 3 | * In the test environment we need some tweaks. 4 | */ 5 | 6 | const tsNode = require('ts-node'); 7 | const tsConfigPaths = require('tsconfig-paths'); 8 | const mainTSConfig = require('./tsconfig.json'); 9 | 10 | tsConfigPaths.register({ 11 | baseUrl: './tests', 12 | paths: { 13 | ...mainTSConfig.compilerOptions.paths, 14 | } 15 | }); 16 | 17 | tsNode.register({ 18 | files: true, 19 | transpileOnly: true, 20 | project: './tsconfig.json' 21 | }); 22 | -------------------------------------------------------------------------------- /src/Common/Entities/InvokeeType.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryColumn, OneToMany, PrimaryGeneratedColumn} from "typeorm"; 2 | import {Activity} from "./Activity"; 3 | import {Invokee} from "../Infrastructure/Atomic"; 4 | 5 | @Entity() 6 | export class InvokeeType { 7 | 8 | @PrimaryGeneratedColumn() 9 | id?: number; 10 | 11 | @Column("varchar", {length: 50}) 12 | name!: Invokee; 13 | 14 | constructor(name?: Invokee) { 15 | if(name !== undefined) { 16 | this.name = name; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Common/Entities/RunStateType.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryColumn, OneToMany, PrimaryGeneratedColumn} from "typeorm"; 2 | import {Activity} from "./Activity"; 3 | import {RunState} from "../Infrastructure/Atomic"; 4 | 5 | @Entity() 6 | export class RunStateType { 7 | 8 | @PrimaryGeneratedColumn() 9 | id?: number; 10 | 11 | @Column("varchar", {length: 50}) 12 | name!: RunState; 13 | 14 | constructor(name?: RunState) { 15 | if(name !== undefined) { 16 | this.name = name; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Web/assets/public/questionsymbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14/tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "resolveJsonModule": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "typeRoots": [ 9 | "./node_modules/@types", 10 | "./src/Web/types", 11 | "./src/Common/Typings" 12 | ] 13 | }, 14 | "include": [ 15 | "**/*.ts", 16 | "**/*.tsx" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "coverage", 21 | "tests/*.ts", 22 | "_site" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/Common/Migrations/Database/Server/1661183583080-submission.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from "typeorm" 2 | import {ActionType} from "../../../Entities/ActionType"; 3 | 4 | export class submission1661183583080 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.manager.getRepository(ActionType).save([ 8 | new ActionType('submission') 9 | ]); 10 | } 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Utils/StringMatching/levenSimilarity.ts: -------------------------------------------------------------------------------- 1 | import leven from "leven"; 2 | 3 | const levenSimilarity = (valA: string, valB: string) => { 4 | let longer: string; 5 | let shorter: string; 6 | if (valA.length > valB.length) { 7 | longer = valA; 8 | shorter = valB; 9 | } else { 10 | longer = valB; 11 | shorter = valA; 12 | } 13 | 14 | const distance = leven(longer, shorter); 15 | const diff = (distance / longer.length) * 100; 16 | return [distance, 100 - diff]; 17 | } 18 | 19 | export default levenSimilarity; 20 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchSubmissionParts.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | { 8 | // triggers if the current activity has more than 0 matches 9 | // if the activity is a submission then matches against title, body, and url 10 | // if "testOn" is not provided then `title, body` are the defaults 11 | "regex": "/fuck|shit|damn/", 12 | "testOn": [ 13 | "title", 14 | "body", 15 | "url" 16 | ] 17 | }, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/paginationJs.ejs: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/Common/WebEntities/ClientSession.ts: -------------------------------------------------------------------------------- 1 | import { ISession } from "connect-typeorm"; 2 | import { Column, Entity, Index, PrimaryColumn, DeleteDateColumn } from "typeorm"; 3 | @Entity() 4 | export class ClientSession implements ISession { 5 | @Index() 6 | @Column("bigint", {transformer: { from: Number, to: Number }}) 7 | public expiredAt = Date.now(); 8 | 9 | @PrimaryColumn("varchar", { length: 255 }) 10 | public id = ""; 11 | 12 | @Column("text") 13 | public json = ""; 14 | 15 | @DeleteDateColumn({ name: 'destroyedAt', nullable: true }) 16 | destroyedAt?: Date; 17 | } 18 | -------------------------------------------------------------------------------- /src/Common/Entities/AuthorEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryColumn, OneToMany} from "typeorm"; 2 | import {Activity} from "./Activity"; 3 | 4 | @Entity({name: 'Author'}) 5 | export class AuthorEntity { 6 | 7 | @Column("varchar", {length: 20, nullable: true}) 8 | id?: string; 9 | 10 | @PrimaryColumn("varchar", {length: 200}) 11 | name!: string; 12 | 13 | @OneToMany(type => Activity, act => act.author) 14 | activities!: Activity[] 15 | 16 | constructor(data?: any) { 17 | if(data !== undefined) { 18 | this.name = data.name; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Common/Entities/Base/TimeAwareRandomBaseEntity.ts: -------------------------------------------------------------------------------- 1 | import {BeforeInsert, Entity, Column, AfterLoad} from "typeorm"; 2 | import dayjs, {Dayjs} from "dayjs"; 3 | import {RandomIdBaseEntity} from "./RandomIdBaseEntity"; 4 | 5 | export abstract class TimeAwareRandomBaseEntity extends RandomIdBaseEntity { 6 | 7 | @Column({ name: 'createdAt', nullable: false }) 8 | _createdAt: Date = new Date(); 9 | 10 | public get createdAt(): Dayjs { 11 | return dayjs(this._createdAt); 12 | } 13 | 14 | public set createdAt(d: Dayjs) { 15 | this._createdAt = d.utc().toDate(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchHistoryActivity.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | // triggers if any activity in the last 10 (including current activity) match the regex 8 | { 9 | "regex": "/fuck|shit|damn/", 10 | // if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 11 | // learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 12 | "window": 10, 13 | }, 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/Common/Entities/RunnableAssociation/CheckToRuleResultEntity.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, ManyToOne} from "typeorm"; 2 | import {RunnableToResultEntity} from "./RunnableToResultEntity"; 3 | import {CheckResultEntity} from "../CheckResultEntity"; 4 | import {RuleResultEntity} from "../RuleResultEntity"; 5 | 6 | 7 | @ChildEntity() 8 | export class CheckToRuleResultEntity extends RunnableToResultEntity { 9 | 10 | @ManyToOne(type => CheckResultEntity, act => act.ruleResults) 11 | runnable?: CheckResultEntity; 12 | 13 | @ManyToOne(type => RuleResultEntity, {cascade: ['insert'], eager: true}) 14 | result!: RuleResultEntity 15 | } 16 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/flairVettedUserSubmission.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Flair Vetted User Submission 4 | description: Flair submission as Approved if user has vet flair 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: newflair 9 | kind: author 10 | # rule will trigger if Author has "vet" flair text 11 | include: 12 | - flairText: 13 | - vet 14 | actions: 15 | - kind: flair 16 | text: Vetted 17 | css: green 18 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/flairNewUserSubmission.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Flair New User Sub 4 | description: Flair submission as sketchy if user does not have vet flair 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: newflair 9 | kind: author 10 | # rule will trigger if Author does not have "vet" flair text 11 | exclude: 12 | - flairText: 13 | - vet 14 | actions: 15 | - kind: flair 16 | text: New User 17 | css: orange 18 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/loadingIcon.ejs: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/AuthorFilterCriteria.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChildEntity, DataSource 3 | } from "typeorm"; 4 | import {FilterCriteria, FilterCriteriaOptions, filterCriteriaTypeIdentifiers} from "./FilterCriteria"; 5 | import objectHash from "object-hash"; 6 | import {AuthorCriteria} from "../../Infrastructure/Filters/FilterCriteria"; 7 | 8 | @ChildEntity() 9 | export class AuthorFilterCriteria extends FilterCriteria { 10 | type: string = filterCriteriaTypeIdentifiers.author; 11 | 12 | constructor(data?: FilterCriteriaOptions) { 13 | super(data); 14 | if(data !== undefined) { 15 | this.generateId(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/instanceTabJs.ejs: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/Common/WebEntities/WebSetting.ts: -------------------------------------------------------------------------------- 1 | import {Entity, PrimaryColumn, Column, DataSource} from "typeorm"; 2 | 3 | export interface WebSettingOptions { 4 | name: string 5 | value: any 6 | } 7 | 8 | @Entity() 9 | export class WebSetting { 10 | 11 | @PrimaryColumn() 12 | name!: string 13 | 14 | @Column({type: 'varchar', length: 255}) 15 | value!: any 16 | 17 | constructor(data?: WebSettingOptions) { 18 | if (data !== undefined) { 19 | this.name = data.name; 20 | this.value = data.value; 21 | } 22 | } 23 | } 24 | 25 | // export const InstanceRepository = (source: DataSource) => source.getRepository(InstanceSetting).extend({ 26 | // get 27 | // }) 28 | -------------------------------------------------------------------------------- /src/Common/Infrastructure/RuleShapes.ts: -------------------------------------------------------------------------------- 1 | import {StructuredRunnableBase} from "./Runnable"; 2 | import {RuleSetConfigObject} from "../../Rule/RuleSet"; 3 | import {RuleObjectJsonTypes} from "../types"; 4 | import {IncludesData} from "./Includes"; 5 | 6 | export type RuleConfigData = RuleObjectJsonTypes | string | IncludesData; 7 | export type RuleConfigHydratedData = Exclude 8 | export type RuleConfigObject = Exclude 9 | export type StructuredRuleConfigObject = Omit & StructuredRunnableBase 10 | export type StructuredRuleSetConfigObject = Omit & { 11 | rules: StructuredRuleConfigObject[] 12 | } 13 | -------------------------------------------------------------------------------- /src/Web/types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import {App} from "../../../App"; 2 | import Bot from "../../../Bot"; 3 | import {Manager} from "../../../Subreddit/Manager"; 4 | import CMUser from "../../Common/User/CMUser"; 5 | import {BotInstance, CMInstanceInterface} from "../../Common/interfaces"; 6 | 7 | declare global { 8 | declare namespace Express { 9 | interface Request { 10 | botApp: App; 11 | logger: Logger; 12 | token?: string, 13 | instance?: CMInstanceInterface, 14 | bot?: BotInstance, 15 | serverBot: Bot, 16 | manager?: Manager, 17 | } 18 | class User extends CMUser { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /heroku.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.14 2 | 3 | ENV TZ=Etc/GMT 4 | 5 | # vips required to run sharp library for image comparison 6 | RUN echo "http://dl-4.alpinelinux.org/alpine/v3.14/community" >> /etc/apk/repositories \ 7 | && apk --update add vips 8 | 9 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 10 | 11 | WORKDIR /usr/app 12 | 13 | COPY package*.json ./ 14 | COPY tsconfig.json . 15 | 16 | RUN npm install 17 | 18 | ADD . /usr/app 19 | 20 | RUN npm run build 21 | 22 | ENV NPM_CONFIG_LOGLEVEL debug 23 | 24 | ARG log_dir=/home/node/logs 25 | RUN mkdir -p $log_dir 26 | VOLUME $log_dir 27 | ENV LOG_DIR=$log_dir 28 | 29 | CMD [ "node", "src/index.js", "run", "all", "--port $PORT"] 30 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/ActivityStateFilterCriteria.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChildEntity, DataSource 3 | } from "typeorm"; 4 | import {FilterCriteria, FilterCriteriaOptions, filterCriteriaTypeIdentifiers} from "./FilterCriteria"; 5 | import objectHash from "object-hash"; 6 | import {TypedActivityState} from "../../Infrastructure/Filters/FilterCriteria"; 7 | 8 | @ChildEntity() 9 | export class ActivityStateFilterCriteria extends FilterCriteria { 10 | type: string = filterCriteriaTypeIdentifiers.activityState; 11 | 12 | constructor(data?: FilterCriteriaOptions) { 13 | super(data); 14 | if(data !== undefined) { 15 | this.generateId(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Common/Entities/Base/TimeAwareAndUpdatedBaseEntity.ts: -------------------------------------------------------------------------------- 1 | import {BeforeInsert, Entity, Column, AfterLoad, BeforeUpdate} from "typeorm"; 2 | import dayjs, {Dayjs} from "dayjs"; 3 | import {TimeAwareBaseEntity} from "./TimeAwareBaseEntity"; 4 | 5 | export abstract class TimeAwareAndUpdatedBaseEntity extends TimeAwareBaseEntity { 6 | 7 | @Column({ name: 'updatedAt', nullable: false }) 8 | _updatedAt: Date = new Date(); 9 | 10 | public get updatedAt(): Dayjs { 11 | return dayjs(this._updatedAt); 12 | } 13 | 14 | public set updatedAt(d: Dayjs) { 15 | this._updatedAt = d.utc().toDate(); 16 | } 17 | 18 | @BeforeUpdate() 19 | public updateAt() { 20 | this.updatedAt = dayjs().utc() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/Entities/RunnableAssociation/RuleSetToRuleResultEntity.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, ManyToOne} from "typeorm"; 2 | import {RunnableToResultEntity} from "./RunnableToResultEntity"; 3 | import {CheckResultEntity} from "../CheckResultEntity"; 4 | import {RuleSetResultEntity} from "../RuleSetResultEntity"; 5 | import {RuleResultEntity} from "../RuleResultEntity"; 6 | 7 | 8 | @ChildEntity() 9 | export class RuleSetToRuleResultEntity extends RunnableToResultEntity { 10 | 11 | @ManyToOne(type => RuleSetResultEntity, act => act._ruleResults) 12 | runnable?: RuleSetResultEntity; 13 | 14 | @ManyToOne(type => RuleResultEntity, {cascade: ['insert'], eager: true}) 15 | result!: RuleResultEntity 16 | } 17 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchActivityThresholdHistory.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | # triggers if more than 3 activities in the last 10 match the regex 5 | - regex: '/fuck|shit|damn/' 6 | # this differs from "totalMatchThreshold" 7 | # 8 | # activityMatchThreshold => # of activities from window must match regex 9 | # totalMatchThreshold => # of matches across all activities from window must match regex 10 | activityMatchThreshold: '> 3' 11 | # if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 12 | # learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 13 | window: 10 14 | -------------------------------------------------------------------------------- /src/Common/Config/YamlConfigDocument.ts: -------------------------------------------------------------------------------- 1 | import AbstractConfigDocument from "./AbstractConfigDocument"; 2 | import {Document, parseDocument} from 'yaml'; 3 | import {ConfigFormat} from "../Infrastructure/Atomic"; 4 | 5 | class YamlConfigDocument extends AbstractConfigDocument { 6 | 7 | public parsed: Document; 8 | public format: ConfigFormat; 9 | 10 | public constructor(raw: string, location?: string) { 11 | super(raw, location); 12 | this.parsed = parseDocument(raw); 13 | this.format = 'yaml'; 14 | } 15 | public toJS(): object { 16 | return this.parsed.toJS(); 17 | } 18 | 19 | public toString(): string { 20 | return this.parsed.toString(); 21 | } 22 | } 23 | 24 | export default YamlConfigDocument; 25 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchTotalHistoryActivity.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | # triggers if there are more than 5 regex matches in the last 10 activities (comments or submission) 5 | - regex: '/fuck|shit|damn/' 6 | # this differs from "activityMatchThreshold" 7 | # 8 | # activityMatchThreshold => # of activities from window must match regex 9 | # totalMatchThreshold => # of matches across all activities from window must match regex 10 | totalMatchThreshold: '> 5' 11 | # if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 12 | # learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 13 | window: 10 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # sassc (a jekll dependency) is very slow to compile bc there are no alpine binaries 4 | # https://github.com/sass/sassc-ruby/issues/189#issuecomment-629758948 5 | # so for now sync used jekyll version with prebuilt binary available in alpine repo 6 | gem "jekyll", "4.2.2" # installed by `gem jekyll` 7 | # gem "webrick" # required when using Ruby >= 3 and Jekyll <= 4.2.2 8 | 9 | gem "just-the-docs", "0.4.0.rc3" # currently the latest pre-release 10 | # gem "just-the-docs" # the latest release - currently 0.3.3 11 | 12 | gem "jekyll-readme-index" 13 | gem 'jekyll-default-layout' 14 | gem 'jekyll-titles-from-headings' 15 | gem 'jekyll-relative-links' 16 | 17 | group :jekyll_plugins do 18 | gem 'jekyll-optional-front-matter' 19 | end 20 | -------------------------------------------------------------------------------- /src/Utils/InvalidRegexError.ts: -------------------------------------------------------------------------------- 1 | import ExtendableError from "es6-error"; 2 | 3 | class InvalidRegexError extends ExtendableError { 4 | constructor(regex: RegExp | RegExp[], val?: string, url?: string, message?: string) { 5 | const msgParts = [ 6 | message ?? 'Regex(es) did not match the value given.', 7 | ]; 8 | let regArr = Array.isArray(regex) ? regex : [regex]; 9 | for(const r of regArr) { 10 | msgParts.push(`Regex: ${r}`) 11 | } 12 | if (val !== undefined) { 13 | msgParts.push(`Value: ${val}`); 14 | } 15 | if (url !== undefined) { 16 | msgParts.push(`Sample regex: ${url}`); 17 | } 18 | super(msgParts.join('\r\n')); 19 | } 20 | } 21 | 22 | export default InvalidRegexError; 23 | -------------------------------------------------------------------------------- /src/Common/Entities/RunnableAssociation/CheckToRuleSetResultEntity.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, ManyToOne} from "typeorm"; 2 | import {RunnableToResultEntity} from "./RunnableToResultEntity"; 3 | import {CheckResultEntity} from "../CheckResultEntity"; 4 | import {RuleResultEntity} from "../RuleResultEntity"; 5 | import {RunResultEntity} from "../RunResultEntity"; 6 | import {RuleSetResultEntity} from "../RuleSetResultEntity"; 7 | 8 | 9 | @ChildEntity() 10 | export class CheckToRuleSetResultEntity extends RunnableToResultEntity { 11 | 12 | @ManyToOne(type => CheckResultEntity, act => act.ruleSetResults) 13 | runnable?: CheckResultEntity; 14 | 15 | @ManyToOne(type => RuleSetResultEntity, {cascade: ['insert'], eager: true}) 16 | result!: RuleSetResultEntity 17 | } 18 | -------------------------------------------------------------------------------- /src/Utils/typeormUtils.ts: -------------------------------------------------------------------------------- 1 | import {SelectQueryBuilder} from "typeorm"; 2 | 3 | export const filterResultsBuilder = (builder: SelectQueryBuilder, rootEntity: string, aliasPrefix: string) => { 4 | return builder 5 | .leftJoinAndSelect(`${rootEntity}._authorIs`, `${aliasPrefix}AuthorIs`) 6 | .leftJoinAndSelect(`${rootEntity}._itemIs`, `${aliasPrefix}ItemIs`) 7 | 8 | .leftJoinAndSelect(`${aliasPrefix}AuthorIs.criteriaResults`, `${aliasPrefix}AuthorCritResults`) 9 | .leftJoinAndSelect(`${aliasPrefix}ItemIs.criteriaResults`, `${aliasPrefix}ItemCritResults`) 10 | 11 | .leftJoinAndSelect(`${aliasPrefix}AuthorCritResults.criteria`, `${aliasPrefix}AuthorCriteria`) 12 | .leftJoinAndSelect(`${aliasPrefix}ItemCritResults.criteria`, `${aliasPrefix}ItemCriteria`) 13 | } 14 | -------------------------------------------------------------------------------- /src/Common/Entities/Subreddit.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryColumn, OneToMany, Index} from "typeorm"; 2 | import {Activity} from "./Activity"; 3 | 4 | export interface SubredditEntityOptions { 5 | id: string 6 | name: string 7 | } 8 | 9 | @Entity() 10 | export class Subreddit { 11 | 12 | @PrimaryColumn() 13 | id!: string; 14 | 15 | @Index({unique: true}) 16 | @Column("varchar", {length: 200}) 17 | name!: string; 18 | 19 | @OneToMany(type => Activity, act => act.subreddit) // note: we will create author property in the Photo class below 20 | activities!: Promise 21 | 22 | constructor(data?: SubredditEntityOptions) { 23 | if (data !== undefined) { 24 | this.id = data.id; 25 | this.name = data.name; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Common/Migrations/Database/Server/1663001719622-subredditInvite.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table, TableColumn} from "typeorm" 2 | 3 | export class subredditInvite1663001719622 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | const table = await queryRunner.getTable('SubredditInvite') as Table; 7 | 8 | await queryRunner.addColumns(table, [ 9 | new TableColumn( { 10 | name: 'messageId', 11 | type: 'varchar', 12 | length: '200', 13 | isUnique: true, 14 | isNullable: true 15 | }), 16 | ]); 17 | } 18 | 19 | public async down(queryRunner: QueryRunner): Promise { 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Common/Entities/Guest/GuestInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { Dayjs } from "dayjs" 2 | import {AuthorEntity} from "../AuthorEntity"; 3 | 4 | export interface Guest { 5 | id: string 6 | name: string 7 | expiresAt?: number 8 | } 9 | 10 | export interface GuestAll { 11 | name: string 12 | expiresAt?: number 13 | subreddits: string[] 14 | } 15 | 16 | 17 | export interface GuestEntityData { 18 | expiresAt?: Dayjs 19 | author: AuthorEntity 20 | } 21 | 22 | export interface HasGuests { 23 | getGuests: () => GuestEntityData[] 24 | addGuest: (val: GuestEntityData | GuestEntityData[]) => GuestEntityData[] 25 | removeGuestById: (val: string | string[]) => GuestEntityData[] 26 | removeGuestByUser: (val: string | string[]) => GuestEntityData[] 27 | removeGuests: () => GuestEntityData[] 28 | } 29 | -------------------------------------------------------------------------------- /src/Utils/AbortToken.ts: -------------------------------------------------------------------------------- 1 | //https://gist.github.com/pygy/6290f78b078e22418821b07d8d63f111#gistcomment-3408351 2 | class AbortToken { 3 | private readonly abortSymbol = Symbol('cancelled'); 4 | private abortPromise: Promise; 5 | private resolve!: Function; // Works due to promise init 6 | 7 | constructor() { 8 | this.abortPromise = new Promise(res => this.resolve = res); 9 | } 10 | 11 | public async wrap(p: PromiseLike): Promise { 12 | const result = await Promise.race([p, this.abortPromise]); 13 | if (result === this.abortSymbol) { 14 | throw new Error('aborted'); 15 | } 16 | 17 | return result; 18 | } 19 | 20 | public abort() { 21 | this.resolve(this.abortSymbol); 22 | } 23 | } 24 | 25 | export default AbortToken; 26 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/1569.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[1569],{1569:(e,t,n)=>{n.r(t),n.d(t,{conf:()=>s,language:()=>o});var s={comments:{lineComment:"#"}},o={defaultToken:"keyword",ignoreCase:!0,tokenPostfix:".azcli",str:/[^#\s]/,tokenizer:{root:[{include:"@comment"},[/\s-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}],[/^-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}]],type:[{include:"@comment"},[/-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":"key.identifier"}}],[/@str+\s*/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}]],comment:[[/#.*$/,{cases:{"@eos":{token:"comment",next:"@popall"}}}]]}}}}]); -------------------------------------------------------------------------------- /src/Common/Config/AbstractConfigDocument.ts: -------------------------------------------------------------------------------- 1 | import {ConfigFormat} from "../Infrastructure/Atomic"; 2 | 3 | export interface ConfigDocumentInterface { 4 | format: ConfigFormat; 5 | parsed: DocumentType 6 | //parsingError: Error | string; 7 | raw: string; 8 | location?: string; 9 | toString(): string; 10 | toJS(): object; 11 | } 12 | 13 | abstract class AbstractConfigDocument implements ConfigDocumentInterface { 14 | public abstract format: ConfigFormat; 15 | public abstract parsed: DocumentType; 16 | //public abstract parsingError: Error | string; 17 | 18 | 19 | constructor(public raw: string, public location?: string) { 20 | } 21 | 22 | 23 | public abstract toString(): string; 24 | public abstract toJS(): object; 25 | } 26 | 27 | export default AbstractConfigDocument; 28 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchActivityThresholdHistory.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | // triggers if more than 3 activities in the last 10 match the regex 8 | { 9 | "regex": "/fuck|shit|damn/", 10 | // this differs from "totalMatchThreshold" 11 | // 12 | // activityMatchThreshold => # of activities from window must match regex 13 | // totalMatchThreshold => # of matches across all activities from window must match regex 14 | "activityMatchThreshold": "> 3", 15 | // if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 16 | // learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 17 | "window": 10, 18 | }, 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/title.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | <% if(locals.title !== undefined) { %> 7 | <%= title %> 8 | <% } %> 9 |
10 | 15 |
16 | 17 | Logout 18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchSubsetHistoryActivity.yaml: -------------------------------------------------------------------------------- 1 | name: swear 2 | kind: regex 3 | criteria: 4 | # triggers if there are more than 5 regex matches in the last 10 activities (comments only) 5 | - regex: '/fuck|shit|damn/' 6 | # this differs from "activityMatchThreshold" 7 | # 8 | # activityMatchThreshold => # of activities from window must match regex 9 | # totalMatchThreshold => # of matches across all activities from window must match regex 10 | totalMatchThreshold: '> 5' 11 | # if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 12 | # learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 13 | window: 10 14 | # determines which activities from window to consider 15 | # defaults to "all" (submissions and comments) 16 | lookAt: comments 17 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/7792.entry.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! ***************************************************************************** 2 | Copyright (c) Microsoft Corporation. All rights reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 8 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 9 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 10 | MERCHANTABLITY OR NON-INFRINGEMENT. 11 | 12 | See the Apache Version 2.0 License for specific language governing permissions 13 | and limitations under the License. 14 | ***************************************************************************** */ 15 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: ContextMod 2 | description: Documentation for ContextMod 3 | theme: just-the-docs 4 | 5 | source: docs 6 | 7 | url: https://contextmod.dev 8 | 9 | color_scheme: dark 10 | 11 | mermaid: 12 | version: "9.1.3" 13 | 14 | aux_links: 15 | Github: https://github.com/foxxmd/context-mod 16 | 17 | plugins: 18 | - jekyll-readme-index 19 | - jekyll-default-layout 20 | - jekyll-titles-from-headings 21 | - jekyll-optional-front-matter 22 | - jekyll-relative-links 23 | 24 | titles_from_headings: 25 | enabled: true 26 | strip_title: false 27 | collections: false 28 | 29 | readme_index: 30 | enabled: true 31 | remove_originals: true 32 | with_frontmatter: true 33 | 34 | optional_front_matter: 35 | remove_originals: true 36 | 37 | defaults: 38 | - scope: 39 | path: "" 40 | values: 41 | has_toc: false 42 | - scope: 43 | path: "images" 44 | values: 45 | image: true 46 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/logSettings.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/ActivityStateFilterCriteriaResult.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, ManyToOne} from "typeorm"; 2 | import {FilterCriteriaResult} from "./FilterCriteriaResult"; 3 | import {ActivityStateFilterCriteria} from "./ActivityStateFilterCriteria"; 4 | import {TypedActivityState} from "../../Infrastructure/Filters/FilterCriteria"; 5 | import {FilterCriteriaResult as IFilterCriteriaResult} from "../../Infrastructure/Filters/FilterShapes"; 6 | 7 | @ChildEntity() 8 | export class ActivityStateFilterCriteriaResult extends FilterCriteriaResult { 9 | 10 | type: string = 'activityState'; 11 | 12 | @ManyToOne(type => ActivityStateFilterCriteria, {cascade: ['insert'], eager: true}) 13 | criteria!: ActivityStateFilterCriteria 14 | 15 | constructor(data?: IFilterCriteriaResult) { 16 | super(data); 17 | if(data !== undefined) { 18 | this.criteria = new ActivityStateFilterCriteria(data.criteria); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/flairVettedUserSubmission.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Flair Vetted User Submission", 7 | "description": "Flair submission as Approved if user has vet flair", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "newflair", 13 | "kind": "author", 14 | // rule will trigger if Author has "vet" flair text 15 | "include": [ 16 | { 17 | "flairText": ["vet"] 18 | } 19 | ] 20 | } 21 | ], 22 | "actions": [ 23 | { 24 | "kind": "flair", 25 | "text": "Vetted", 26 | "css": "green" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchTotalHistoryActivity.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | // triggers if there are more than 5 regex matches in the last 10 activities (comments or submission) 8 | { 9 | // triggers if there are more than 5 *total matches* across the last 10 activities 10 | "regex": "/fuck|shit|damn/", 11 | // this differs from "activityMatchThreshold" 12 | // 13 | // activityMatchThreshold => # of activities from window must match regex 14 | // totalMatchThreshold => # of matches across all activities from window must match regex 15 | "totalMatchThreshold": "> 5", 16 | // if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 17 | // learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 18 | "window": 10, 19 | }, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/Check/SubmissionCheck.ts: -------------------------------------------------------------------------------- 1 | import {Check, CheckOptions} from "./index"; 2 | import { 3 | RuleResult, 4 | UserResultCache 5 | } from "../Common/interfaces"; 6 | import {Submission, Comment} from "snoowrap/dist/objects"; 7 | import {buildFilter} from "../util"; 8 | import {FilterOptions, MinimalOrFullFilter} from "../Common/Infrastructure/Filters/FilterShapes"; 9 | import {SubmissionState} from "../Common/Infrastructure/Filters/FilterCriteria"; 10 | import {ActivityType} from "../Common/Infrastructure/Reddit"; 11 | 12 | export interface SubmissionCheckOptions extends CheckOptions { 13 | itemIs?: MinimalOrFullFilter 14 | } 15 | 16 | export class SubmissionCheck extends Check { 17 | itemIs: FilterOptions; 18 | checkType = 'submission' as ActivityType; 19 | 20 | constructor(options: SubmissionCheckOptions) { 21 | super(options); 22 | const {itemIs = []} = options; 23 | this.itemIs = buildFilter(itemIs); 24 | this.logSummary(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Common/Config/JsonConfigDocument.ts: -------------------------------------------------------------------------------- 1 | import AbstractConfigDocument from "./AbstractConfigDocument"; 2 | import {stringify, parse} from 'comment-json'; 3 | import JSON5 from 'json5'; 4 | import {OperatorJsonConfig} from "../interfaces"; 5 | import {ConfigFormat} from "../Infrastructure/Atomic"; 6 | 7 | class JsonConfigDocument extends AbstractConfigDocument { 8 | 9 | public parsed: OperatorJsonConfig; 10 | protected cleanParsed: OperatorJsonConfig; 11 | public format: ConfigFormat; 12 | 13 | public constructor(raw: string, location?: string) { 14 | super(raw, location); 15 | this.parsed = parse(raw) as OperatorJsonConfig; 16 | this.cleanParsed = JSON5.parse(raw); 17 | this.format = 'json'; 18 | } 19 | 20 | public toJS(): OperatorJsonConfig { 21 | return this.cleanParsed; 22 | } 23 | 24 | public toString(): string { 25 | return stringify(this.parsed, null, 1); 26 | } 27 | 28 | } 29 | 30 | export default JsonConfigDocument; 31 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/flairNewUserSubmission.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Flair New User Sub", 7 | "description": "Flair submission as sketchy if user does not have vet flair", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "newflair", 13 | "kind": "author", 14 | // rule will trigger if Author does not have "vet" flair text 15 | "exclude": [ 16 | { 17 | "flairText": ["vet"] 18 | } 19 | ] 20 | } 21 | ], 22 | "actions": [ 23 | { 24 | "kind": "flair", 25 | "text": "New User", 26 | "css": "orange" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/history/lowEngagement.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Low Comment Engagement 4 | description: Check if Author is submitting much more than they comment 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: lowComm 9 | kind: history 10 | criteria: 11 | - comment: '< 30%' 12 | window: 13 | # get author's last 90 days of activities or 100 activities, whichever is less 14 | duration: 90 days 15 | count: 100 16 | # trigger if less than 30% of their activities in this time period are comments 17 | 18 | actions: 19 | - kind: report 20 | content: >- 21 | Low engagement: comments were {{rules.lowcomm.commentPercent}} of 22 | {{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}} 23 | -------------------------------------------------------------------------------- /.idea/redditcontextbot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Common/Entities/Base/TimeAwareBaseEntity.ts: -------------------------------------------------------------------------------- 1 | import {BeforeInsert, Entity, Column, AfterLoad, BeforeUpdate} from "typeorm"; 2 | import dayjs, {Dayjs} from "dayjs"; 3 | 4 | export abstract class TimeAwareBaseEntity { 5 | 6 | @Column({ name: 'createdAt', nullable: false }) 7 | _createdAt: Date = new Date(); 8 | 9 | public get createdAt(): Dayjs { 10 | return dayjs(this._createdAt); 11 | } 12 | 13 | public set createdAt(d: Dayjs) { 14 | this._createdAt = d.utc().toDate(); 15 | } 16 | 17 | toJSON() { 18 | const jsonObj: any = Object.assign({}, this); 19 | const proto = Object.getPrototypeOf(this); 20 | for (const key of Object.getOwnPropertyNames(proto)) { 21 | const desc = Object.getOwnPropertyDescriptor(proto, key); 22 | const hasGetter = desc && typeof desc.get === 'function'; 23 | if (hasGetter) { 24 | jsonObj[key] = (this as any)[key]; 25 | } 26 | } 27 | return jsonObj; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 FoxxMD 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/AuthorFilterResult.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, ManyToOne, OneToMany} from "typeorm"; 2 | import {FilterResult} from "./FilterResult"; 3 | import {AuthorFilterCriteriaResult} from "./FilterCriteriaResult"; 4 | import {AuthorCriteria} from "../../Infrastructure/Filters/FilterCriteria"; 5 | import { 6 | FilterCriteriaResult as IFilterCriteriaResult, 7 | FilterResult as IFilterResult 8 | } from "../../Infrastructure/Filters/FilterShapes"; 9 | 10 | @ChildEntity() 11 | export class AuthorFilterResult extends FilterResult { 12 | 13 | type: string = 'author'; 14 | 15 | @OneToMany(type => AuthorFilterCriteriaResult, obj => obj.filterResult, {cascade: ['insert'], eager: true}) 16 | criteriaResults!: AuthorFilterCriteriaResult[] 17 | 18 | constructor(data?: IFilterResult) { 19 | super(data); 20 | if(data !== undefined) { 21 | this.criteriaResults = data.criteriaResults.map(x => new AuthorFilterCriteriaResult(x)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/history/opOnlyEngagement.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Engaging Own Content Only 4 | description: Check if Author is mostly engaging in their own content only 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: opOnly 9 | kind: history 10 | criteria: 11 | # trigger if more than 60% of their activities in this time period are comments as OP 12 | - comment: '> 60% OP' 13 | window: 14 | # get author's last 90 days of activities or 100 activities, whichever is less 15 | duration: 90 days 16 | count: 100 17 | 18 | actions: 19 | - kind: report 20 | content: >- 21 | Selfish OP: {{rules.oponly.opPercent}} of 22 | {{rules.oponly.commentTotal}} comments over {{rules.oponly.window}} 23 | are as OP 24 | -------------------------------------------------------------------------------- /src/Common/Entities/Transformers/index.ts: -------------------------------------------------------------------------------- 1 | import { ValueTransformer } from "typeorm" 2 | import {Duration} from "dayjs/plugin/duration.js"; 3 | import {isNullOrUndefined} from "../../../util"; 4 | import dayjs from "dayjs"; 5 | 6 | /** 7 | * @see https://github.com/typeorm/typeorm/issues/873#issuecomment-424643086 8 | * 9 | * */ 10 | export class ColumnDurationTransformer implements ValueTransformer { 11 | to(data?: Duration): number | undefined { 12 | if (!isNullOrUndefined(data)) { 13 | return data.asSeconds(); 14 | } 15 | return undefined; 16 | } 17 | 18 | from(data?: number | null): Duration | undefined { 19 | if (!isNullOrUndefined(data)) { 20 | return dayjs.duration(data, 'seconds') 21 | } 22 | return undefined 23 | } 24 | } 25 | 26 | export class ColumnDecimalTransformer implements ValueTransformer { 27 | to(data: number): number { 28 | return data; 29 | } 30 | from(data: string): number { 31 | return parseFloat(data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/userNotes/usernoteSP.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Self Promo Activities 4 | # check will run on a new submission in your subreddit and look at the Author of that submission 5 | description: >- 6 | Check if any of Author's aggregated submission origins are >10% of entire 7 | history 8 | kind: submission 9 | rules: 10 | - name: attr10all 11 | kind: attribution 12 | criteria: 13 | - threshold: '> 10%' 14 | window: 90 days 15 | - threshold: '> 10%' 16 | window: 100 17 | actions: 18 | - kind: usernote 19 | # the key of usernote type 20 | # https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 21 | type: spamwarn 22 | content: >- 23 | Self Promotion: {{rules.attr10all.titlesDelim}} 24 | {{rules.attr10sub.largestPercent}}% 25 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/4896.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[4896],{4896:(e,n,s)=>{s.r(n),s.d(n,{conf:()=>o,language:()=>t});var o={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},t={defaultToken:"",tokenPostfix:".ini",escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^\[[^\]]*\]/,"metatag"],[/(^\w+)(\s*)(\=)/,["key","","delimiter"]],{include:"@whitespace"},[/\d+/,"number"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],whitespace:[[/[ \t\r\n]+/,""],[/^\s*[#;].*$/,"comment"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}}}}]); -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/regex/matchSubsetHistoryActivity.json5: -------------------------------------------------------------------------------- 1 | // goes inside 2 | // "rules": [] 3 | { 4 | "name": "swear", 5 | "kind": "regex", 6 | "criteria": [ 7 | // triggers if there are more than 5 regex matches in the last 10 activities (comments only) 8 | { 9 | "regex": "/fuck|shit|damn/", 10 | // this differs from "activityMatchThreshold" 11 | // 12 | // activityMatchThreshold => # of activities from window must match regex 13 | // totalMatchThreshold => # of matches across all activities from window must match regex 14 | "totalMatchThreshold": "> 5", 15 | // if `window` is specified it tells the rule to check the current activity as well as the activities returned from `window` 16 | // learn more about `window` here https://github.com/FoxxMD/context-mod/blob/master/docs/activitiesWindow.md 17 | "window": 10, 18 | // determines which activities from window to consider 19 | //defaults to "all" (submissions and comments) 20 | "lookAt": "comments", 21 | }, 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/Common/Entities/RunEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryGeneratedColumn, OneToMany, ManyToOne, PrimaryColumn} from "typeorm"; 2 | import {ManagerEntity} from "./ManagerEntity"; 3 | import {Activity} from "./Activity"; 4 | import {RunResultEntity} from "./RunResultEntity"; 5 | import {CheckEntity} from "./CheckEntity"; 6 | 7 | export interface RunEntityOptions { 8 | name: string 9 | manager: ManagerEntity 10 | } 11 | 12 | @Entity({name: 'Run'}) 13 | export class RunEntity { 14 | 15 | @PrimaryColumn("varchar", {length: 300}) 16 | name!: string; 17 | 18 | @ManyToOne(type => ManagerEntity, act => act.runs) 19 | manager!: ManagerEntity; 20 | 21 | @OneToMany(type => RunResultEntity, obj => obj.run, {cascade: ['insert']}) 22 | results!: RunResultEntity[] 23 | 24 | @OneToMany(type => CheckEntity, obj => obj.run) 25 | checks!: CheckEntity[] 26 | 27 | constructor(data?: RunEntityOptions) { 28 | if(data !== undefined) { 29 | this.name = data.name; 30 | this.manager = data.manager; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/repeatActivity/crosspostSpamming.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Crosspost Spam 4 | description: Check if Author is spamming Submissions across subreddits 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: xpostspam 9 | kind: repeatActivity 10 | # will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by 11 | useSubmissionAsReference: true 12 | # if the Author has posted this Submission 5 times consecutively then this rule will trigger 13 | threshold: '>= 5' 14 | # look at all of the Author's submissions in the last 7 days 15 | window: 7 days 16 | actions: 17 | - kind: report 18 | content: >- 19 | Author has posted this link {{rules.xpostspam.largestRepeat}} times 20 | over {{rules.xpostspam.window}} 21 | -------------------------------------------------------------------------------- /src/Web/Common/User/CMUser.ts: -------------------------------------------------------------------------------- 1 | import {IUser} from "../interfaces"; 2 | 3 | export interface ClientUserData { 4 | token?: string 5 | tokenExpiresAt?: number 6 | scope?: string[] 7 | webOperator?: boolean 8 | } 9 | 10 | abstract class CMUser implements IUser { 11 | constructor(public name: string, public subreddits: string[], public clientData: ClientUserData = {}) { 12 | 13 | } 14 | 15 | public abstract isInstanceOperator(val: Instance): boolean; 16 | public abstract canAccessInstance(val: Instance): boolean; 17 | public abstract canAccessBot(val: Bot): boolean; 18 | public abstract accessibleBots(bots: Bot[]): Bot[] 19 | public abstract canAccessSubreddit(val: Bot, name: string): boolean; 20 | public abstract accessibleSubreddits(bot: Bot): SubredditEntity[] 21 | public abstract isSubredditGuest(val: Bot, name: string): boolean; 22 | public abstract isSubredditMod(val: Bot, name: string): boolean; 23 | public abstract getModeratedSubreddits(val: Bot): SubredditEntity[] 24 | } 25 | 26 | export default CMUser; 27 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/ActivityStateFilterResult.ts: -------------------------------------------------------------------------------- 1 | import {ChildEntity, OneToMany} from "typeorm"; 2 | import {FilterResult} from "./FilterResult"; 3 | import {ActivityStateFilterCriteriaResult} from "./ActivityStateFilterCriteriaResult"; 4 | import {TypedActivityState} from "../../Infrastructure/Filters/FilterCriteria"; 5 | import { 6 | FilterCriteriaResult as IFilterCriteriaResult, 7 | FilterResult as IFilterResult 8 | } from "../../Infrastructure/Filters/FilterShapes"; 9 | 10 | @ChildEntity() 11 | export class ActivityStateFilterResult extends FilterResult { 12 | 13 | type: string = 'activityState'; 14 | 15 | @OneToMany(type => ActivityStateFilterCriteriaResult, obj => obj.filterResult, {cascade: ['insert'], eager: true}) 16 | criteriaResults!: ActivityStateFilterCriteriaResult[] 17 | 18 | constructor(data?: IFilterResult) { 19 | super(data); 20 | if(data !== undefined) { 21 | this.criteriaResults = data.criteriaResults.map(x => new ActivityStateFilterCriteriaResult(x)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web/assets/views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%- include('partials/head', {title: 'CM'}) %> 3 | 4 |
5 | <%- include('partials/title', {title: 'Error'}) %> 6 |
7 |
8 |
9 |
10 |
Oops 😬
11 |
12 |
Something went wrong while processing that last request:
13 |
<%- error %>
14 | <% if(locals.operatorDisplay !== undefined && locals.operatorDisplay !== 'Anonymous') { %> 15 |
Operated By: <%= operatorDisplay %>
16 | <% } %> 17 |
18 |
19 |
20 |
21 |
22 | <%- include('partials/footer') %> 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Common/Infrastructure/Database.ts: -------------------------------------------------------------------------------- 1 | import {LoggerOptions} from "typeorm/logger/LoggerOptions"; 2 | import {SqljsConnectionOptions} from "typeorm/driver/sqljs/SqljsConnectionOptions"; 3 | import {MysqlConnectionOptions} from "typeorm/driver/mysql/MysqlConnectionOptions"; 4 | import {PostgresConnectionOptions} from "typeorm/driver/postgres/PostgresConnectionOptions"; 5 | import {BetterSqlite3ConnectionOptions} from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions"; 6 | 7 | export type DatabaseDriverType = 'sqljs' | 'better-sqlite3' | 'mysql' | 'mariadb' | 'postgres'; 8 | export type DatabaseConfig = 9 | SqljsConnectionOptions 10 | | MysqlConnectionOptions 11 | | PostgresConnectionOptions 12 | | BetterSqlite3ConnectionOptions; 13 | export type DatabaseDriverConfig = { 14 | type: DatabaseDriverType, 15 | [key: string]: any 16 | /** 17 | * Set the type of logging typeorm should output 18 | * 19 | * Defaults to errors, warnings, and schema (migration progress) 20 | * */ 21 | logging?: LoggerOptions 22 | } 23 | export type DatabaseDriver = DatabaseDriverType | DatabaseDriverConfig; 24 | -------------------------------------------------------------------------------- /src/SubredditConfigData.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityCheckConfigValue, 3 | } from "./Check"; 4 | import {ManagerOptions} from "./Common/interfaces"; 5 | import {RunConfigHydratedData, RunConfigValue, RunConfigObject} from "./Run"; 6 | 7 | export interface SubredditConfigData extends ManagerOptions { 8 | /** 9 | * A list of all the checks that should be run for a subreddit. 10 | * 11 | * Checks are split into two lists -- submission or comment -- based on kind and run independently. 12 | * 13 | * Checks in each list are run in the order found in the configuration. 14 | * 15 | * When a check "passes", and actions are performed, then all subsequent checks are skipped. 16 | * @minItems 1 17 | * */ 18 | checks?: ActivityCheckConfigValue[] 19 | 20 | /** 21 | * A list of sets of Checks to run 22 | * @minItems 1 23 | * */ 24 | runs?: RunConfigValue[] 25 | } 26 | 27 | export interface SubredditConfigHydratedData extends Omit { 28 | runs?: RunConfigHydratedData[] 29 | } 30 | 31 | export interface SubredditConfigObject extends SubredditConfigHydratedData { 32 | runs?: RunConfigObject[] 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/history/lowEngagement.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Low Comment Engagement", 7 | "description": "Check if Author is submitting much more than they comment", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "lowComm", 13 | "kind": "history", 14 | "criteria": [ 15 | { 16 | // look at last 90 days of Author's activities 17 | "window": "90 days", 18 | // trigger if less than 30% of their activities in this time period are comments 19 | "comment": "< 30%" 20 | }, 21 | ] 22 | } 23 | ], 24 | "actions": [ 25 | { 26 | "kind": "report", 27 | "content": "Low engagement: comments were {{rules.lowcomm.commentPercent}} of {{rules.lowcomm.activityTotal}} over {{rules.lowcomm.window}}" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/history/opOnlyEngagement.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Engaging Own Content Only", 7 | "description": "Check if Author is mostly engaging in their own content only", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "opOnly", 13 | "kind": "history", 14 | "criteria": [ 15 | { 16 | // look at last 90 days of Author's activities 17 | "window": "90 days", 18 | // trigger if more than 60% of their activities in this time period are comments as OP 19 | "comment": "> 60% OP" 20 | }, 21 | ] 22 | } 23 | ], 24 | "actions": [ 25 | { 26 | "kind": "report", 27 | "content": "Selfish OP: {{rules.oponly.opPercent}} of {{rules.oponly.commentTotal}} comments over {{rules.oponly.window}} are as OP" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/Common/Entities/FilterCriteria/FilterResult.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryColumn, OneToMany, PrimaryGeneratedColumn, ManyToOne, TableInheritance} from "typeorm"; 2 | import {RandomIdBaseEntity} from "../Base/RandomIdBaseEntity"; 3 | import {JoinOperands} from "../../Infrastructure/Atomic"; 4 | import {FilterCriteriaResult as IFilterCriteriaResult} from "../../Infrastructure/Filters/FilterShapes"; 5 | //import {FilterCriteriaResult} from "./FilterCriteriaResult"; 6 | 7 | export interface FilterResultOptions { 8 | passed: boolean 9 | join: JoinOperands 10 | } 11 | 12 | @Entity() 13 | @TableInheritance({column: {type: "varchar", name: "type"}}) 14 | export abstract class FilterResult extends RandomIdBaseEntity { 15 | 16 | 17 | // @OneToMany(type => FilterCriteriaResult, obj => obj.filterResult, {cascade: ['insert']}) 18 | //criteriaResults!: FilterCriteriaResult[] 19 | 20 | @Column("varchar", {length: 200}) 21 | join!: JoinOperands 22 | 23 | @Column("boolean") 24 | passed!: boolean 25 | 26 | constructor(data?: FilterResultOptions) { 27 | super(); 28 | if (data !== undefined) { 29 | this.join = data.join; 30 | this.passed = data.passed 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/commentSpam.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newComm 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Remove comments by users who spam the same comment many times 10 | # 11 | - name: low xp comment spam 12 | description: X-posted comment >=4x 13 | kind: comment 14 | rules: 15 | - name: xPostLowComm 16 | kind: repeatActivity 17 | # number of "non-repeat" comments allowed between "repeat comments" 18 | gapAllowance: 2 19 | # greater or more than 4 repeat comments triggers this rule 20 | threshold: '>= 4' 21 | # retrieve either last 50 comments or 6 months' of history, whichever is less 22 | window: 23 | count: 100 24 | duration: 6 months 25 | actions: 26 | - kind: report 27 | enable: false 28 | content: 'Remove => Posted same comment {{rules.xpostlowcomm.largestRepeat}}x times' 29 | 30 | - kind: remove 31 | enable: true 32 | note: 'Posted same comment {{rules.xpostlowcomm.largestRepeat}}x times' 33 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/submissionRepost.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newSub 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | - name: BotRepost 9 | description: Remove submission if it is likely a repost 10 | kind: submission 11 | rules: 12 | # search reddit for similar submissions to see if it is a repost 13 | - name: subRepost 14 | kind: repost 15 | criteria: 16 | - searchOn: 17 | # match found Submissions sameness using title against title of Submission being checked 18 | - kind: title 19 | # sameness (confidence) % of a title required to consider Submission being checked as a repost 20 | matchScore: 90 21 | 22 | actions: 23 | # report the submission 24 | - kind: report 25 | enable: true 26 | content: '{{rules.subrepost.closestSameness}} confidence this is a repost.' 27 | 28 | # remove the submission 29 | - kind: remove 30 | enable: false 31 | note: '{{rules.subrepost.closestSameness}} confidence this is a repost.' 32 | -------------------------------------------------------------------------------- /src/Common/Entities/CheckEntity.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryGeneratedColumn, OneToMany, ManyToOne, PrimaryColumn} from "typeorm"; 2 | import {CheckResultEntity} from "./CheckResultEntity"; 3 | import {RunEntity} from "./RunEntity"; 4 | import {ManagerEntity} from "./ManagerEntity"; 5 | import {ActivityType} from "../Infrastructure/Reddit"; 6 | 7 | export interface CheckEntityOptions { 8 | name: string 9 | type: ActivityType 10 | run: RunEntity 11 | manager: ManagerEntity 12 | } 13 | 14 | @Entity({name: 'Check'}) 15 | export class CheckEntity { 16 | 17 | @PrimaryColumn("varchar", {length: 300}) 18 | name!: string; 19 | 20 | @Column("varchar", {length: 20}) 21 | type!: ActivityType 22 | 23 | @OneToMany(type => CheckResultEntity, obj => obj.run, {cascade: ['insert']}) 24 | results!: CheckResultEntity[] 25 | 26 | @ManyToOne(type => RunEntity, act => act.checks) 27 | run!: RunEntity; 28 | 29 | @ManyToOne(type => ManagerEntity, act => act.checks) 30 | manager!: ManagerEntity; 31 | 32 | constructor(data?: CheckEntityOptions) { 33 | if (data !== undefined) { 34 | this.name = data.name; 35 | this.type = data.type; 36 | this.run = data.run; 37 | this.manager = data.manager; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/floodingNewSubmissions.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newSub 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Add a mote note to users who are making more than 4 submissions a day 10 | # and optionally remove new submissions by them 11 | # 12 | - name: Flooding New 13 | description: Detect users make more than 4 submission in 24 hours 14 | kind: submission 15 | rules: 16 | - name: Recent In Sub 17 | kind: recentActivity 18 | useSubmissionAsReference: false 19 | window: 20 | duration: 24 hours 21 | fetch: submissions 22 | thresholds: 23 | - subreddits: 24 | # change this to your subreddit 25 | - MYSUBREDDIT 26 | threshold: "> 4" 27 | actions: 28 | - kind: modnote 29 | type: SPAM_WATCH 30 | content: '{{rules.recentinsub.totalCount}} submissions in the last 24 hours' 31 | 32 | - kind: remove 33 | enable: false 34 | note: '{{rules.recentinsub.totalCount}} submissions in the last 24 hours' 35 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/recentActivity/freeKarma.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Free Karma Alert 4 | description: Check if author has posted in 'freekarma' subreddits 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: freekarma 9 | kind: recentActivity 10 | # // when lookAt is not present this rule will look for submissions and comments 11 | #lookAt: comments 12 | useSubmissionAsReference: false 13 | thresholds: 14 | # if the number of activities (sub/comment) found CUMULATIVELY in the subreddits listed is 15 | # equal to or greater than 1 then the rule is triggered 16 | - threshold: '>= 1' 17 | subreddits: 18 | - DeFreeKarma 19 | - FreeKarma4U 20 | - FreeKarma4You 21 | - upvote 22 | window: 7 days 23 | actions: 24 | - kind: report 25 | content: >- 26 | {{rules.freekarma.totalCount}} activities in karma 27 | {{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}: 28 | {{rules.freekarma.subSummary}} 29 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/repeatActivity/burstPosting.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Burstpost Spam 4 | description: Check if Author is crossposting in short bursts 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: burstpost 9 | kind: repeatActivity 10 | # will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by 11 | useSubmissionAsReference: true 12 | # the number of non-repeat activities (submissions or comments) to ignore between repeat submissions 13 | gapAllowance: 3 14 | # if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger 15 | threshold: '>= 6' 16 | # look at all of the Author's submissions in the last 7 days or 100 submissions 17 | window: 18 | duration: 7 days 19 | count: 100 20 | actions: 21 | - kind: report 22 | content: >- 23 | Author has burst-posted this link {{rules.burstpost.largestRepeat}} 24 | times over {{rules.burstpost.window}} 25 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/repeatActivity/crosspostSpamming.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Crosspost Spam", 7 | "description": "Check if Author is spamming Submissions across subreddits", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "xpostspam", 13 | "kind": "repeatActivity", 14 | // will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by 15 | "useSubmissionAsReference": true, 16 | // if the Author has posted this Submission 5 times consecutively then this rule will trigger 17 | "threshold": ">= 5", 18 | // look at all of the Author's submissions in the last 7 days 19 | "window": "7 days" 20 | } 21 | ], 22 | "actions": [ 23 | { 24 | "kind": "report", 25 | "content": "Author has posted this link {{rules.xpostspam.largestRepeat}} times over {{rules.xpostspam.window}}" 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /patches/snoowrap+1.23.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/snoowrap/dist/request_handler.js b/node_modules/snoowrap/dist/request_handler.js 2 | index 5a31f40..90cd5cb 100644 3 | --- a/node_modules/snoowrap/dist/request_handler.js 4 | +++ b/node_modules/snoowrap/dist/request_handler.js 5 | @@ -144,6 +144,30 @@ function oauthRequest(options) { 6 | } 7 | 8 | throw e; 9 | + }).catch(function(e) { 10 | + const validCodes = _this._config.timeoutCodes || []; 11 | + if(validCodes.length === 0 || attempts >= _this._config.maxRetryAttempts) { 12 | + throw e; 13 | + } 14 | + // collect codes 15 | + const codes = []; 16 | + if(e.code !== undefined) { 17 | + codes.push(e.code); 18 | + } 19 | + if(e.cause !== undefined && e.cause.code !== undefined) { 20 | + codes.push(e.cause.code); 21 | + } 22 | + if(codes.length === 0) { 23 | + throw e; 24 | + } 25 | + 26 | + const validCode = codes.find(x => validCodes.includes(x)); 27 | + if(validCode === undefined) { 28 | + throw e; 29 | + } 30 | + 31 | + _this._warn(`Got error with valid retry code (${validCode}) from request attempt to reddit -- ${e.message} --`, "Retrying request (attempt ".concat(attempts + 1, "/").concat(_this._config.maxRetryAttempts, ")...")); 32 | + return _this.oauthRequest(options, attempts + 1); 33 | }); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/sexSolicitationHistory.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newSub 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Remove submission if user has any "redditor for [sex]..." submissions in their history 10 | # and optionally bans user 11 | # 12 | - name: sexSpamHistory 13 | description: Detect sex spam language in recent history and ban if found (most likely a bot) 14 | kind: submission 15 | rules: 16 | - kind: regex 17 | name: redditorFor 18 | criteria: 19 | # matches if text has common "looking for" acronym like F4M R4A etc... 20 | - regex: '/[RFM]4[a-zA-Z\s0-9]/i' 21 | totalMatchThreshold: "> 1" 22 | window: 100 23 | testOn: 24 | - body 25 | - title 26 | actions: 27 | - kind: remove 28 | enable: true 29 | note: 'Has sex solicitation submission history: {{rules.redditorfor.matchSample}}' 30 | 31 | - kind: modnote 32 | type: ABUSE_WARNING 33 | content: 'Has sex solicitation submission history: {{rules.redditorfor.matchSample}}' 34 | -------------------------------------------------------------------------------- /docker/config/docker-compose/config.yaml: -------------------------------------------------------------------------------- 1 | operator: 2 | name: CHANGE_THIS #YOUR REDDIT USERNAME HERE 3 | logging: 4 | # default level for all transports 5 | level: debug 6 | file: 7 | # override default level 8 | level: warn 9 | # true -> log folder at projectDir/log 10 | dirname: true 11 | caching: 12 | provider: 13 | store: redis 14 | host: cache 15 | port: 6379 16 | prefix: prod 17 | databaseConfig: 18 | migrations: 19 | continueOnAutomatedBackup: true 20 | #force: true # uncomment this to make cm run new migrations without confirmation 21 | #logging: ['query', 'error', 'warn', 'log'] # uncomment this to get typeorm to log EVERYTHING 22 | connection: 23 | type: 'mariadb' 24 | host: 'database' 25 | username: 'cmuser' 26 | # This should match the password set in docker-compose.yaml 27 | password: 'CHANGE_THIS' 28 | database: 'ContextMod' 29 | web: 30 | credentials: 31 | redirectUri: 'http://localhost:8085/callback' 32 | session: 33 | storage: cache 34 | port: 8085 35 | # 36 | # Influx/Grafana requires additional configuration. See https://github.com/FoxxMD/context-mod/blob/master/docs/operator/database.md#influx 37 | # 38 | #influxConfig: 39 | # credentials: 40 | # url: 'http://influx:8086' 41 | # token: 'YourInfluxToken' 42 | # org: YourInfluxOrg 43 | # bucket: contextmod 44 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/attribution/redditSelfPromoSubmissionOnly.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Self Promo Submissions 4 | description: >- 5 | Check if any of Author's aggregated submission origins are >10% of their 6 | submissions 7 | # check will run on a new submission in your subreddit and look at the Author of that submission 8 | kind: submission 9 | rules: 10 | - name: attr10sub 11 | kind: attribution 12 | # criteria defaults to OR -- so either of these criteria will trigger the rule 13 | criteria: 14 | - threshold: '> 10%' # threshold can be a percent or an absolute number 15 | thresholdOn: submissions # calculate percentage of submissions, rather than entire history (submissions & comments) 16 | window: 90 days # look at last 90 days of Author's activities (comments and submissions) 17 | - threshold: '> 10%' 18 | thresholdOn: submissions 19 | window: 100 # look at Author's last 100 activities (comments and submissions) 20 | actions: 21 | - kind: report 22 | content: >- 23 | {{rules.attr10sub.largestPercent}}% of 24 | {{rules.attr10sub.activityTotal}} items over 25 | {{rules.attr10sub.window}} 26 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/9741.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[9741],{9741:(t,e,r)=>{r.r(e),r.d(e,{conf:()=>s,language:()=>o});var s={brackets:[],autoClosingPairs:[],surroundingPairs:[]},o={keywords:[],typeKeywords:[],tokenPostfix:".csp",operators:[],symbols:/[=>= 3' 34 | lookAt: comments 35 | window: 10 36 | actions: 37 | - kind: remove 38 | -------------------------------------------------------------------------------- /src/Common/Entities/EntityRunState/EntityRunState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryColumn, 5 | OneToMany, 6 | PrimaryGeneratedColumn, 7 | OneToOne, 8 | TableInheritance, 9 | ManyToOne 10 | } from "typeorm"; 11 | import {InvokeeType} from "../InvokeeType"; 12 | import {RunStateType} from "../RunStateType"; 13 | import {RunningState} from "../../../Subreddit/Manager"; 14 | 15 | export interface EntityRunStateOptions { 16 | invokee: InvokeeType 17 | runType: RunStateType 18 | } 19 | 20 | @Entity() 21 | @TableInheritance({column: {type: "varchar", name: "type"}}) 22 | export abstract class EntityRunState { 23 | 24 | @PrimaryGeneratedColumn() 25 | id?: number; 26 | 27 | @OneToOne(() => InvokeeType) 28 | @ManyToOne(() => InvokeeType, undefined,{eager: true}) 29 | invokee!: InvokeeType 30 | 31 | @OneToOne(() => RunStateType) 32 | @ManyToOne(() => RunStateType, undefined,{eager: true}) 33 | runType!: RunStateType 34 | 35 | constructor(data?: EntityRunStateOptions) { 36 | if (data !== undefined) { 37 | this.invokee = data.invokee; 38 | this.runType = data.runType; 39 | } 40 | } 41 | 42 | toRunningState(): RunningState { 43 | return { 44 | state: this.runType.name, 45 | causedBy: this.invokee.name 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/recentActivity/freeKarmaOnSubmission.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Free Karma On Submission Alert 4 | description: Check if author has posted this submission in 'freekarma' subreddits 5 | kind: submission 6 | rules: 7 | - name: freekarmasub 8 | kind: recentActivity 9 | # rule will only look at Author's submissions in these subreddits 10 | lookAt: submissions 11 | # rule will only look at Author's submissions in these subreddits that have the same content (link) as the submission this event was made on 12 | # In simpler terms -- rule will only check to see if the same link the author just posted is also posted in these subreddits 13 | useSubmissionAsReference: true 14 | thresholds: 15 | - threshold: '>= 1' 16 | subreddits: 17 | - DeFreeKarma 18 | - FreeKarma4U 19 | - FreeKarma4You 20 | - upvote 21 | window: 7 days 22 | actions: 23 | - kind: report 24 | content: >- 25 | Submission posted {{rules.freekarmasub.totalCount}} times in karma 26 | {{rules.freekarmasub.subCount}} subs over 27 | {{rules.freekarmasub.window}}: {{rules.freekarmasub.subSummary}} 28 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/attribution/redditSelfPromoAll.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Self Promo Activities 4 | description: >- 5 | Check if any of Author's aggregated submission origins are >10% of entire 6 | history 7 | # check will run on a new submission in your subreddit and look at the Author of that submission 8 | kind: submission 9 | rules: 10 | - name: attr10all 11 | kind: attribution 12 | # criteria defaults to OR -- so either of these criteria will trigger the rule 13 | criteria: 14 | - threshold: '> 10%' # threshold can be a percent or an absolute number 15 | # The default is "all" -- calculate percentage of entire history (submissions & comments) 16 | #thresholdOn: all 17 | # 18 | # look at last 90 days of Author's activities (comments and submissions) 19 | window: 90 days 20 | - threshold: '> 10%' 21 | # look at Author's last 100 activities (comments and submissions) 22 | window: 100 23 | actions: 24 | - kind: report 25 | content: >- 26 | {{rules.attr10all.largestPercent}}% of 27 | {{rules.attr10all.activityTotal}} items over 28 | {{rules.attr10all.window}} 29 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/userNotes/usernoteFilter.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Self Promo Activities 4 | description: Tag SP only if user does not have good contributor user note 5 | # check will run on a new submission in your subreddit and look at the Author of that submission 6 | kind: submission 7 | rules: 8 | - name: attr10all 9 | kind: attribution 10 | author: 11 | exclude: 12 | # the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 13 | # rule will not run if current usernote on Author is of type 'gooduser' 14 | - type: gooduser 15 | criteria: 16 | - threshold: '> 10%' 17 | window: 90 days 18 | - threshold: '> 10%' 19 | window: 100 20 | actions: 21 | - kind: usernote 22 | # the key of usernote type 23 | # https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 24 | type: spamwarn 25 | # content is mustache templated 26 | content: >- 27 | Self Promotion: {{rules.attr10all.titlesDelim}} 28 | {{rules.attr10sub.largestPercent}}% 29 | -------------------------------------------------------------------------------- /src/Common/ActivitySource.ts: -------------------------------------------------------------------------------- 1 | import {ActivitySourceData, ActivitySourceTypes} from "./Infrastructure/Atomic"; 2 | import {strToActivitySourceData} from "../util"; 3 | 4 | export class ActivitySource { 5 | type: ActivitySourceTypes 6 | identifier?: string 7 | 8 | constructor(data: string | ActivitySourceData) { 9 | if (typeof data === 'string') { 10 | const {type, identifier} = strToActivitySourceData(data); 11 | this.type = type; 12 | this.identifier = identifier; 13 | } else { 14 | this.type = data.type; 15 | this.identifier = data.identifier; 16 | } 17 | } 18 | 19 | matches(desired: ActivitySource): boolean { 20 | if(desired.type !== this.type) { 21 | return false; 22 | } 23 | // if this source does not have an identifier (we have already matched type) then it is broad enough to match 24 | if(this.identifier === undefined) { 25 | return true; 26 | } 27 | // at this point we know this source has an identifier but desired DOES NOT so this source is more restrictive and does not match 28 | if(desired.identifier === undefined) { 29 | return false; 30 | } 31 | // otherwise sources match if identifiers are the same 32 | return this.identifier.toLowerCase() === desired.identifier.toLowerCase(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Common/Entities/Stats/TotalStat.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; 2 | import {ManagerEntity} from "../ManagerEntity"; 3 | import dayjs, {Dayjs} from "dayjs"; 4 | import {TimeAwareBaseEntity} from "../Base/TimeAwareBaseEntity"; 5 | import {ColumnDecimalTransformer} from "../Transformers"; 6 | 7 | export interface TotalStatOptions { 8 | metric: string 9 | value: number 10 | manager: ManagerEntity 11 | createdAt?: Dayjs 12 | } 13 | 14 | @Entity() 15 | export class TotalStat extends TimeAwareBaseEntity{ 16 | 17 | @PrimaryGeneratedColumn() 18 | id!: number; 19 | 20 | @Column("varchar", {length: 60}) 21 | metric!: string; 22 | 23 | @Column({type: 'decimal', precision: 12, scale: 2, transformer: new ColumnDecimalTransformer()}) 24 | value!: number 25 | 26 | @ManyToOne(type => ManagerEntity) 27 | @JoinColumn({name: 'managerId'}) 28 | manager!: ManagerEntity; 29 | 30 | @Column() 31 | managerId!: string 32 | 33 | constructor(data?: TotalStatOptions) { 34 | super(); 35 | if (data !== undefined) { 36 | this.metric = data.metric; 37 | this.value = data.value; 38 | this.manager = data.manager; 39 | if (data.createdAt !== undefined) { 40 | this.createdAt = data.createdAt 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Notification/DiscordNotifier.ts: -------------------------------------------------------------------------------- 1 | import webhook from 'webhook-discord'; 2 | import {NotificationContent} from "../Common/interfaces"; 3 | 4 | class DiscordNotifier { 5 | name: string 6 | botName: string 7 | type: string = 'Discord'; 8 | url: string; 9 | 10 | constructor(name: string, botName: string, url: string) { 11 | this.name = name; 12 | this.url = url; 13 | this.botName = botName; 14 | } 15 | 16 | async handle(val: NotificationContent) { 17 | const h = new webhook.Webhook(this.url); 18 | 19 | const hook = new webhook.MessageBuilder(); 20 | 21 | const {logLevel, title, footer, body = ''} = val; 22 | 23 | hook.setName(this.botName === 'ContextMod' ? 'ContextMod' : `(ContextMod) ${this.botName}`) 24 | .setTitle(title) 25 | .setDescription(body) 26 | 27 | if (footer !== undefined) { 28 | // @ts-ignore 29 | hook.setFooter(footer, false); 30 | } 31 | 32 | switch (logLevel) { 33 | case 'error': 34 | hook.setColor("##ff0000"); 35 | break; 36 | case 'warn': 37 | hook.setColor("#ffe900"); 38 | break; 39 | default: 40 | hook.setColor("#00fffa"); 41 | break; 42 | } 43 | 44 | await h.send(hook); 45 | } 46 | } 47 | 48 | export default DiscordNotifier; 49 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/diametricSpam.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newSub 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | - name: diametricSpam 9 | description: Check if author has posted the same image in opposite subs 10 | kind: submission 11 | rules: 12 | - name: recent 13 | kind: recentActivity 14 | useSubmissionAsReference: true 15 | # requires your subreddit to be running on a CM instance that supports image processing 16 | imageDetection: 17 | enable: true 18 | threshold: 5 19 | lookAt: submissions 20 | window: 30 21 | thresholds: 22 | - threshold: ">= 1" 23 | subreddits: 24 | - AnotherSubreddit 25 | actions: 26 | - kind: remove 27 | enable: true 28 | content: "Posted same image in {{rules.recent.subSummary}}" 29 | 30 | - kind: comment 31 | distinguish: true 32 | sticky: true 33 | lock: true 34 | content: 'You have posted the same image in another subreddit ({{rules.recent.subSummary}}) that does not make sense given the theme of this subreddit. We consider this spam and it has been removed.' 35 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/transcribersOfReddit.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newComm 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Detect top-level comments by users from r/transcribersofreddit 10 | # and approve/flair the user 11 | # 12 | - name: transcriber comment 13 | description: approve/flair transcribed video comment 14 | kind: comment 15 | itemIs: 16 | # top-level comments 17 | depth: '< 1' 18 | condition: AND 19 | rules: 20 | - name: transcribedVideoFormat 21 | kind: regex 22 | criteria: 23 | - regex: '/^[\n\r\s]*\*Video Transcription\*[\n\r]+---[\S\s]+---/gim' 24 | - name: transcribersActivity 25 | kind: recentActivity 26 | window: 27 | count: 100 28 | duration: 1 week 29 | useSubmissionAsReference: false 30 | thresholds: 31 | - subreddits: 32 | - transcribersofreddit 33 | actions: 34 | - kind: approve 35 | - name: flairTranscriber 36 | kind: flair 37 | authorIs: 38 | exclude: 39 | - flairText: 40 | - Transcriber ✍️ 41 | text: Transcriber ✍️ 42 | -------------------------------------------------------------------------------- /src/Common/Entities/Base/RandomIdBaseEntity.ts: -------------------------------------------------------------------------------- 1 | import {BeforeInsert, Entity, PrimaryColumn} from "typeorm"; 2 | import {nanoid} from "nanoid"; 3 | 4 | export abstract class RandomIdBaseEntity { 5 | 6 | /* 7 | * Wanted to use a random id that was smaller than a UUID since all these entities will (probably) be accessed through api endpoints at some point 8 | * and they need to be url friendly 9 | * 10 | * ID Example: 4rTFo6yoQIFT6UTr 11 | * */ 12 | 13 | 14 | @PrimaryColumn('varchar', {length: 20}) 15 | id!: string 16 | 17 | constructor() { 18 | this.id = nanoid(16); 19 | } 20 | 21 | @BeforeInsert() 22 | setId() { 23 | if(this.id === undefined) { 24 | this.id = nanoid(16); 25 | } 26 | } 27 | 28 | toJSON() { 29 | const jsonObj: any = Object.assign({}, this); 30 | const proto = Object.getPrototypeOf(this); 31 | for (const key of Object.getOwnPropertyNames(proto)) { 32 | const desc = Object.getOwnPropertyDescriptor(proto, key); 33 | const hasGetter = desc && typeof desc.get === 'function'; 34 | if (hasGetter) { 35 | jsonObj[key] = (this as any)[key]; 36 | const regKey = `_${key}`; 37 | if(jsonObj[regKey] !== undefined) { 38 | delete jsonObj[regKey]; 39 | } 40 | } 41 | } 42 | return jsonObj; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/userNotes/usernoteSP.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Self Promo Activities", 7 | "description": "Check if any of Author's aggregated submission origins are >10% of entire history", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "attr10all", 13 | "kind": "attribution", 14 | "criteria": [ 15 | { 16 | "threshold": "> 10%", 17 | "window": "90 days" 18 | }, 19 | { 20 | "threshold": "> 10%", 21 | "window": 100 22 | } 23 | ], 24 | } 25 | ], 26 | "actions": [ 27 | { 28 | "kind": "usernote", 29 | // the key of usernote type 30 | // https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 31 | "type": "spamwarn", 32 | // content is mustache templated as usual 33 | "content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/Common/defaults.ts: -------------------------------------------------------------------------------- 1 | import {HistoricalStatsDisplay} from "./interfaces"; 2 | import path from "path"; 3 | import {FilterCriteriaDefaults} from "./Infrastructure/Filters/FilterShapes"; 4 | 5 | export const dayjsDTFormat = 'YYYY-MM-DD HH:mm:ssZ'; 6 | export const dayjsTimeFormat = 'HH:mm:ss z'; 7 | 8 | export const cacheOptDefaults = {ttl: 60, max: 500, checkPeriod: 600}; 9 | export const cacheTTLDefaults = { 10 | authorTTL: 60, 11 | userNotesTTL: 300, 12 | modNotesTTL: 60, 13 | wikiTTL: 300, 14 | submissionTTL: 60, 15 | commentTTL: 60, 16 | filterCriteriaTTL: 60, 17 | subredditTTL: 600, 18 | selfTTL: 60 19 | }; 20 | 21 | export const createHistoricalDisplayDefaults = (): HistoricalStatsDisplay => ({ 22 | checksRunTotal: 0, 23 | checksFromCacheTotal: 0, 24 | checksTriggeredTotal: 0, 25 | rulesRunTotal: 0, 26 | rulesCachedTotal: 0, 27 | rulesTriggeredTotal: 0, 28 | actionsRunTotal: 0, 29 | eventsCheckedTotal: 0, 30 | eventsActionedTotal: 0, 31 | }) 32 | 33 | export const filterCriteriaDefault: FilterCriteriaDefaults = { 34 | authorIs: { 35 | exclude: [ 36 | { 37 | criteria: { 38 | isMod: true 39 | } 40 | } 41 | ] 42 | } 43 | } 44 | 45 | export const defaultDataDir = path.resolve(__dirname, '../..'); 46 | export const defaultConfigFilenames = ['config.json', 'config.yaml']; 47 | 48 | export const VERSION = '0.13.2'; 49 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/8975.entry.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e,r,t={8975:(e,r,t)=>{var o=t(7223),n=t(3488);let s=!1;self.onmessage=e=>{s||function(e){if(s)return;s=!0;const r=new o._i((e=>{self.postMessage(e)}),(e=>new n.ky(e,null)));self.onmessage=e=>{r.onmessage(e.data)}}()}}},o={};function n(e){var r=o[e];if(void 0!==r)return r.exports;var s=o[e]={exports:{}};return t[e](s,s.exports,n),s.exports}n.m=t,n.x=()=>{var e=n.O(void 0,[4200],(()=>n(8975)));return n.O(e)},e=[],n.O=(r,t,o,s)=>{if(!t){var i=1/0;for(p=0;p=s)&&Object.keys(n.O).every((e=>n.O[e](t[f])))?t.splice(f--,1):(a=!1,s0&&e[p-1][2]>s;p--)e[p]=e[p-1];e[p]=[t,o,s]},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.f={},n.e=e=>Promise.all(Object.keys(n.f).reduce(((r,t)=>(n.f[t](e,r),r)),[])),n.u=e=>e+".entry.js",n.miniCssF=e=>{},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),n.p="/public/yaml/",(()=>{var e={8975:1};n.f.i=(r,t)=>{e[r]||importScripts(n.p+n.u(r))};var r=self.webpackChunkdemo=self.webpackChunkdemo||[],t=r.push.bind(r);r.push=r=>{var[o,s,i]=r;for(var a in s)n.o(s,a)&&(n.m[a]=s[a]);for(i&&i(n);o.length;)e[o.pop()]=1;t(r)}})(),r=n.x,n.x=()=>n.e(4200).then(r),n.x()})(); -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/repeatActivity/burstPosting.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Burstpost Spam", 7 | "description": "Check if Author is crossposting in short bursts", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "burstpost", 13 | "kind": "repeatActivity", 14 | // will only look at Submissions in Author's history that contain the same content (link) as the Submission this check was initiated by 15 | "useSubmissionAsReference": true, 16 | // the number of non-repeat activities (submissions or comments) to ignore between repeat submissions 17 | "gapAllowance": 3, 18 | // if the Author has posted this Submission 6 times, ignoring 3 non-repeat activities between each repeat, then this rule will trigger 19 | "threshold": ">= 6", 20 | // look at all of the Author's submissions in the last 7 days 21 | "window": "7 days" 22 | } 23 | ], 24 | "actions": [ 25 | { 26 | "kind": "report", 27 | "content": "Author has burst-posted this link {{rules.burstpost.largestRepeat}} times over {{rules.burstpost.window}}" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ], 34 | } 35 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= locals.title !== undefined ? title : `${locals.botName !== undefined ? `CM for ${botName}` : 'ContextMod'}`%> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Common/Entities/Stats/TimeSeriesStat.ts: -------------------------------------------------------------------------------- 1 | import {Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; 2 | import {ManagerEntity} from "../ManagerEntity"; 3 | import dayjs, {Dayjs} from "dayjs"; 4 | import {TotalStatOptions} from "./TotalStat"; 5 | import {TimeAwareBaseEntity} from "../Base/TimeAwareBaseEntity"; 6 | import {ColumnDecimalTransformer} from "../Transformers"; 7 | 8 | export interface TimeSeriesStatOptions extends TotalStatOptions { 9 | granularity: string 10 | } 11 | 12 | @Entity() 13 | export class TimeSeriesStat extends TimeAwareBaseEntity { 14 | 15 | @PrimaryGeneratedColumn() 16 | id!: number; 17 | 18 | @Column() 19 | granularity!: string 20 | 21 | @Column("varchar", {length: 60}) 22 | metric!: string; 23 | 24 | @Column({type: 'decimal', precision: 12, scale: 2, transformer: new ColumnDecimalTransformer()}) 25 | value!: number 26 | 27 | @ManyToOne(type => ManagerEntity) 28 | @JoinColumn({name: 'managerId'}) 29 | manager!: ManagerEntity; 30 | 31 | @Column() 32 | managerId!: string 33 | 34 | constructor(data?: TimeSeriesStatOptions) { 35 | super(); 36 | if (data !== undefined) { 37 | this.metric = data.metric; 38 | this.value = data.value; 39 | this.manager = data.manager; 40 | this.granularity = data.granularity; 41 | if (data.createdAt !== undefined) { 42 | this.createdAt = data.createdAt 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | ContextMod <%= locals.applicationIdentifier%> created by /u/FoxxMD 4 |
5 |
6 | 7 | 33 | -------------------------------------------------------------------------------- /src/Common/Entities/RunnableAssociation/RunnableToResultEntity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | PrimaryGeneratedColumn, 5 | ManyToOne, 6 | OneToOne, 7 | CreateDateColumn, 8 | JoinColumn, 9 | OneToMany, TableInheritance, AfterLoad 10 | } from "typeorm"; 11 | import {CheckResultEntity} from "../CheckResultEntity"; 12 | import {TimeAwareRandomBaseEntity} from "../Base/TimeAwareRandomBaseEntity"; 13 | import {RuleResultEntity} from "../RuleResultEntity"; 14 | import {RuleSetResultEntity} from "../RuleSetResultEntity"; 15 | import {RunResultEntity} from "../RunResultEntity"; 16 | 17 | export interface RunnableToRuleResultEntityOptions { 18 | result: U 19 | order: number 20 | runnable: T 21 | } 22 | 23 | @Entity({name: 'RunnableResult'}) 24 | @TableInheritance({column: {type: "varchar", name: "type"}}) 25 | export abstract class RunnableToResultEntity extends TimeAwareRandomBaseEntity { 26 | 27 | @Column() 28 | order!: number 29 | 30 | // @ManyToOne(type => RuleResultEntity, {cascade: ['insert'], eager: true}) 31 | result!: U 32 | 33 | // @ManyToOne(type => T, act => act.runnable) 34 | runnable?: T; 35 | 36 | constructor(data?: RunnableToRuleResultEntityOptions) { 37 | super(); 38 | if (data !== undefined) { 39 | this.order = data.order; 40 | this.runnable = data.runnable; 41 | this.result = data.result; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Common/Migrations/Database/Web/1660588028346-removeInvites.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner} from "typeorm" 2 | import {tableHasData} from "../MigrationUtil"; 3 | 4 | export class removeInvites1660588028346 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const dbType = queryRunner.connection.driver.options.type; 8 | 9 | if (dbType === 'sqljs' && await queryRunner.hasTable('Invite')) { 10 | // const countRes = await queryRunner.query('select count(*) from Invite'); 11 | // let hasNoRows = null; 12 | // if (Array.isArray(countRes) && countRes[0] !== null) { 13 | // const { 14 | // 'count(*)': count 15 | // } = countRes[0] || {}; 16 | // hasNoRows = count === 0; 17 | // } 18 | 19 | const hasRows = await tableHasData(queryRunner, 'Invite'); 20 | 21 | if (hasRows === false) { 22 | await queryRunner.dropTable('Invite'); 23 | } else { 24 | let prefix = hasRows === null ? `Could not determine if SQL.js 'web' database had the table 'Invite' --` : `SQL.js 'web' database had the table 'Invite' and it is not empty --` 25 | queryRunner.connection.logger.logSchemaBuild(`${prefix} This table is being replaced by 'BotInvite' table in 'app' database. If you have existing invites you will need to recreate them.`); 26 | } 27 | } 28 | } 29 | 30 | public async down(queryRunner: QueryRunner): Promise { 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/4369.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[4369],{4369:(e,o,t)=>{t.r(o),t.d(o,{conf:()=>n,language:()=>s});var n={comments:{lineComment:"#"},brackets:[["[","]"],["<",">"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"<",close:">"},{open:"(",close:")"}],surroundingPairs:[{open:"[",close:"]"},{open:"<",close:">"},{open:"(",close:")"}]},s={defaultToken:"",tokenPostfix:".pla",brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"<",close:">",token:"delimiter.angle"},{open:"(",close:")",token:"delimiter.parenthesis"}],keywords:[".i",".o",".mv",".ilb",".ob",".label",".type",".phase",".pair",".symbolic",".symbolic-output",".kiss",".p",".e",".end"],comment:/#.*$/,identifier:/[a-zA-Z]+[a-zA-Z0-9_\-]*/,plaContent:/[01\-~\|]+/,tokenizer:{root:[{include:"@whitespace"},[/@comment/,"comment"],[/\.([a-zA-Z_\-]+)/,{cases:{"@eos":{token:"keyword.$1"},"@keywords":{cases:{".type":{token:"keyword.$1",next:"@type"},"@default":{token:"keyword.$1",next:"@keywordArg"}}},"@default":{token:"keyword.$1"}}}],[/@identifier/,"identifier"],[/@plaContent/,"string"]],whitespace:[[/[ \t\r\n]+/,""]],type:[{include:"@whitespace"},[/\w+/,{token:"type",next:"@pop"}]],keywordArg:[[/[ \t\r\n]+/,{cases:{"@eos":{token:"",next:"@pop"},"@default":""}}],[/@comment/,"comment","@pop"],[/[<>()\[\]]/,{cases:{"@eos":{token:"@brackets",next:"@pop"},"@default":"@brackets"}}],[/\-?\d+/,{cases:{"@eos":{token:"number",next:"@pop"},"@default":"number"}}],[/@identifier/,{cases:{"@eos":{token:"identifier",next:"@pop"},"@default":"identifier"}}],[/[;=]/,{cases:{"@eos":{token:"delimiter",next:"@pop"},"@default":"delimiter"}}]]}}}}]); -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/onlyfansFlair.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: Flair OF submitters 4 | description: Flair submission as OF if user does not have Verified flair and has 5 | certain keywords in their profile 6 | kind: submission 7 | authorIs: 8 | exclude: 9 | - flairCssClass: 10 | - verified 11 | rules: 12 | - name: OnlyFans strings in description 13 | kind: author 14 | include: 15 | - description: 16 | - '/(cashapp|allmylinks|linktr|onlyfans\.com)/i' 17 | - '/(see|check|my|view) (out|of|onlyfans|kik|skype|insta|ig|profile|links)/i' 18 | - my links 19 | - "$" 20 | actions: 21 | - name: Set OnlyFans user flair 22 | kind: userflair 23 | flair_template_id: put-your-onlyfans-user-flair-id-here 24 | - name: Set OF Creator SUBMISSION flair 25 | kind: flair 26 | flair_template_id: put-your-onlyfans-post-flair-id-here 27 | - name: Flair posts of OF submitters 28 | description: Flair submission as OnlyFans if submitter has OnlyFans userflair (override post flair set by submitter) 29 | kind: submission 30 | rules: 31 | - name: Include OF submitters 32 | kind: author 33 | include: 34 | - flairCssClass: 35 | - onlyfans 36 | actions: 37 | - name: Set OF Creator SUBMISSION flair 38 | kind: flair 39 | flair_template_id: put-your-onlyfans-post-flair-id-here 40 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/freekarma.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newSub 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Remove submissions from users who have recent activity in freekarma subs in the last 100 activities 10 | # 11 | - name: freekarma removal 12 | description: Remove submission if user has used freekarma sub recently 13 | kind: submission 14 | rules: 15 | - name: freekarma 16 | kind: recentActivity 17 | window: 100 18 | useSubmissionAsReference: false 19 | thresholds: 20 | - subreddits: 21 | - FreeKarma4U 22 | - FreeKarma4You 23 | - freekarmaforyou 24 | - KarmaFarming4Pros 25 | - KarmaStore 26 | - upvote 27 | - promote 28 | - shamelessplug 29 | - upvote 30 | - FreeUpVotes 31 | - GiveMeKarma 32 | - nsfwkarma 33 | - GetFreeKarmaAnyTime 34 | - freekarma2021 35 | - FreeKarma2022 36 | - KarmaRocket 37 | - FREEKARMA4PORN 38 | actions: 39 | - kind: report 40 | enable: false 41 | content: 'Remove => {{rules.freekarma.totalCount}} activities in freekarma subs' 42 | 43 | - kind: remove 44 | enable: true 45 | note: '{{rules.freekarma.totalCount}} activities in freekarma subs' 46 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/6330.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[6330],{6330:(e,o,n)=>{n.r(o),n.d(o,{conf:()=>t,language:()=>s});var t={comments:{lineComment:";",blockComment:["#|","|#"]},brackets:[["(",")"],["{","}"],["[","]"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}]},s={defaultToken:"",ignoreCase:!0,tokenPostfix:".scheme",brackets:[{open:"(",close:")",token:"delimiter.parenthesis"},{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"}],keywords:["case","do","let","loop","if","else","when","cons","car","cdr","cond","lambda","lambda*","syntax-rules","format","set!","quote","eval","append","list","list?","member?","load"],constants:["#t","#f"],operators:["eq?","eqv?","equal?","and","or","not","null?"],tokenizer:{root:[[/#[xXoObB][0-9a-fA-F]+/,"number.hex"],[/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?/,"number.float"],[/(?:\b(?:(define|define-syntax|define-macro))\b)(\s+)((?:\w|\-|\!|\?)*)/,["keyword","white","variable"]],{include:"@whitespace"},{include:"@strings"},[/[a-zA-Z_#][a-zA-Z0-9_\-\?\!\*]*/,{cases:{"@keywords":"keyword","@constants":"constant","@operators":"operators","@default":"identifier"}}]],comment:[[/[^\|#]+/,"comment"],[/#\|/,"comment","@push"],[/\|#/,"comment","@pop"],[/[\|#]/,"comment"]],whitespace:[[/[ \t\r\n]+/,"white"],[/#\|/,"comment","@comment"],[/;.*$/,"comment"]],strings:[[/"$/,"string","@popall"],[/"(?=.)/,"string","@multiLineString"]],multiLineString:[[/[^\\"]+$/,"string","@popall"],[/[^\\"]+/,"string"],[/\\./,"string.escape"],[/"/,"string","@popall"],[/\\$/,"string"]]}}}}]); -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/brigadingNoHistory.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newComm 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # Report comments from users with no history in the subreddit IF the submission is flaired as being brigaded 10 | # optionally, remove comment 11 | # 12 | - name: Brigading No History 13 | kind: comment 14 | # only runs on comments in a submission with a link flair css class of 'brigaded' 15 | itemIs: 16 | - submissionState: 17 | # can use any or all of these to detect brigaded submission 18 | - link_flair_css: brigaded 19 | #flairTemplate: 123-1234 20 | #link_flair_text: Restricted 21 | rules: 22 | - name: noHistory 23 | kind: recentActivity 24 | # check last 100 activities that have not been removed 25 | window: 26 | count: 100 27 | filterOn: 28 | post: 29 | commentState: 30 | include: 31 | - removed: false 32 | thresholds: 33 | # triggers if user has only one activity (this one) in your subreddit 34 | - subreddits: 35 | - MYSUBREDDIT 36 | threshold: '<= 1' 37 | actions: 38 | - kind: report 39 | enable: true 40 | content: User has no history in subreddit 41 | 42 | - kind: remove 43 | enable: false 44 | note: User has no history in subreddit 45 | -------------------------------------------------------------------------------- /src/Subreddit/ModNotes/ModUserNote.ts: -------------------------------------------------------------------------------- 1 | import {Comment, PrivateMessage, RedditUser, Submission} from "snoowrap/dist/objects"; 2 | import {ModUserNoteLabel} from "../../Common/Infrastructure/Atomic"; 3 | //import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients"; 4 | import {generateSnoowrapEntityFromRedditThing, parseRedditFullname} from "../../util"; 5 | import Snoowrap from "snoowrap"; 6 | 7 | export interface ModUserNoteRaw { 8 | note?: string | null 9 | reddit_id?: string | null 10 | label?: string | null 11 | } 12 | 13 | export class ModUserNote { 14 | note?: string 15 | actedOn?: RedditUser | Submission | Comment | PrivateMessage 16 | label?: ModUserNoteLabel 17 | 18 | constructor(data: ModUserNoteRaw | undefined, client: Snoowrap) { 19 | const { 20 | note, 21 | reddit_id, 22 | label 23 | } = data || {}; 24 | this.note = note !== null ? note : undefined; 25 | this.label = label !== null ? label as ModUserNoteLabel : undefined; 26 | 27 | if (reddit_id !== null && reddit_id !== undefined) { 28 | const thing = parseRedditFullname(reddit_id); 29 | if (thing !== undefined) { 30 | this.actedOn = generateSnoowrapEntityFromRedditThing(thing, client) as RedditUser | Submission | Comment; 31 | } 32 | } 33 | } 34 | 35 | toRaw(): ModUserNoteRaw { 36 | return { 37 | note: this.note, 38 | reddit_id: this.actedOn !== undefined ? this.actedOn.id : undefined, 39 | label: this.label 40 | } 41 | } 42 | 43 | toJSON() { 44 | return this.toRaw(); 45 | } 46 | } 47 | 48 | export default ModUserNote; 49 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/attribution/redditSelfPromoAll.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Self Promo Activities", 7 | "description": "Check if any of Author's aggregated submission origins are >10% of entire history", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "attr10all", 13 | "kind": "attribution", 14 | // criteria defaults to OR -- so either of these criteria will trigger the rule 15 | "criteria": [ 16 | { 17 | // threshold can be a percent or an absolute number 18 | "threshold": "> 10%", 19 | // The default is "all" -- calculate percentage of entire history (submissions & comments) 20 | // "thresholdOn": "all", 21 | 22 | // look at last 90 days of Author's activities (comments and submissions) 23 | "window": "90 days" 24 | }, 25 | { 26 | "threshold": "> 10%", 27 | // look at Author's last 100 activities (comments and submissions) 28 | "window": 100 29 | } 30 | ], 31 | } 32 | ], 33 | "actions": [ 34 | { 35 | "kind": "report", 36 | "content": "{{rules.attr10all.largestPercent}}% of {{rules.attr10all.activityTotal}} items over {{rules.attr10all.window}}" 37 | } 38 | ] 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/recentActivity/freeKarma.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Free Karma Alert", 7 | "description": "Check if author has posted in 'freekarma' subreddits", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "freekarma", 13 | "kind": "recentActivity", 14 | "useSubmissionAsReference": false, 15 | // when `lookAt` is not present this rule will look for submissions and comments 16 | // lookAt: "submissions" 17 | // lookAt: "comments" 18 | "thresholds": [ 19 | { 20 | // for all subreddits, if the number of activities (sub/comment) is equal to or greater than 1 then the rule is triggered 21 | "threshold": ">= 1", 22 | "subreddits": [ 23 | "DeFreeKarma", 24 | "FreeKarma4U", 25 | "FreeKarma4You", 26 | "upvote" 27 | ] 28 | } 29 | ], 30 | // will look at all of the Author's activities in the last 7 days 31 | "window": "7 days" 32 | } 33 | ], 34 | "actions": [ 35 | { 36 | "kind": "report", 37 | "content": "{{rules.freekarma.totalCount}} activities in karma {{rules.freekarma.subCount}} subs over {{rules.freekarma.window}}: {{rules.freekarma.subSummary}}" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/9482.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[9482],{9482:(e,o,n)=>{n.r(o),n.d(o,{conf:()=>s,language:()=>t});var s={comments:{blockComment:["/*","*/"],lineComment:"//"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string"]},{open:"[",close:"]",notIn:["string"]},{open:"(",close:")",notIn:["string"]},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}]},t={defaultToken:"",tokenPostfix:".flow",keywords:["import","require","export","forbid","native","if","else","cast","unsafe","switch","default"],types:["io","mutable","bool","int","double","string","flow","void","ref","true","false","with"],operators:["=",">","<","<=",">=","==","!","!=",":=","::=","&&","||","+","-","*","/","@","&","%",":","->","\\","$","??","^"],symbols:/[@$=>](?!@symbols)/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/((0(x|X)[0-9a-fA-F]*)|(([0-9]+\.?[0-9]*)|(\.[0-9]+))((e|E)(\+|-)?[0-9]+)?)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]]}}}}]); -------------------------------------------------------------------------------- /src/Common/Subreddit/SubredditResourceInterfaces.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityDispatch, 3 | CacheConfig, 4 | Footer, 5 | StrongTTLConfig, 6 | ThirdPartyCredentialsJsonConfig, 7 | TTLConfig 8 | } from "../interfaces"; 9 | import {Cache} from "cache-manager"; 10 | import {Subreddit} from "snoowrap/dist/objects"; 11 | import {DataSource} from "typeorm"; 12 | import {Logger} from "winston"; 13 | import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients"; 14 | import {ManagerEntity} from "../Entities/ManagerEntity"; 15 | import {Bot} from "../Entities/Bot"; 16 | import {EventRetentionPolicyRange, StatisticFrequencyOption} from "../Infrastructure/Atomic"; 17 | import {CMCache} from "../Cache"; 18 | 19 | export interface SubredditResourceOptions extends Footer { 20 | ttl: StrongTTLConfig 21 | cache: CMCache 22 | cacheType: string; 23 | cacheSettingsHash: string 24 | subreddit: Subreddit, 25 | database: DataSource 26 | logger: Logger; 27 | client: ExtendedSnoowrap; 28 | prefix?: string; 29 | thirdPartyCredentials: ThirdPartyCredentialsJsonConfig 30 | delayedItems?: ActivityDispatch[] 31 | botAccount?: string 32 | botName: string 33 | managerEntity: ManagerEntity 34 | botEntity: Bot 35 | statFrequency: StatisticFrequencyOption 36 | retention?: EventRetentionPolicyRange 37 | footer?: false | string 38 | } 39 | 40 | export interface SubredditResourceConfig extends Footer { 41 | caching?: CacheConfig, 42 | subreddit: Subreddit, 43 | logger: Logger; 44 | client: ExtendedSnoowrap 45 | credentials?: ThirdPartyCredentialsJsonConfig 46 | managerEntity: ManagerEntity 47 | botEntity: Bot 48 | statFrequency: StatisticFrequencyOption 49 | retention?: EventRetentionPolicyRange 50 | } 51 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/1594.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[1594],{1594:(e,o,n)=>{n.r(o),n.d(o,{conf:()=>t,language:()=>r});var t={comments:{lineComment:"'"},brackets:[["(",")"],["[","]"],["If","EndIf"],["While","EndWhile"],["For","EndFor"],["Sub","EndSub"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]}]},r={defaultToken:"",tokenPostfix:".sb",ignoreCase:!0,brackets:[{token:"delimiter.array",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"keyword.tag-if",open:"If",close:"EndIf"},{token:"keyword.tag-while",open:"While",close:"EndWhile"},{token:"keyword.tag-for",open:"For",close:"EndFor"},{token:"keyword.tag-sub",open:"Sub",close:"EndSub"}],keywords:["Else","ElseIf","EndFor","EndIf","EndSub","EndWhile","For","Goto","If","Step","Sub","Then","To","While"],tagwords:["If","Sub","While","For"],operators:[">","<","<>","<=",">=","And","Or","+","-","*","/","="],identifier:/[a-zA-Z_][\w]*/,symbols:/[=><:+\-*\/%\.,]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[{include:"@whitespace"},[/(@identifier)(?=[.])/,"type"],[/@identifier/,{cases:{"@keywords":{token:"keyword.$0"},"@operators":"operator","@default":"variable.name"}}],[/([.])(@identifier)/,{cases:{$2:["delimiter","type.member"],"@default":""}}],[/\d*\.\d+/,"number.float"],[/\d+/,"number"],[/[()\[\]]/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":"delimiter"}}],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"]],whitespace:[[/[ \t\r\n]+/,""],[/(\').*$/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"C?/,"string","@pop"]]}}}}]); -------------------------------------------------------------------------------- /src/Web/assets/views/error-authenticated.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%- include('partials/head', {title: 'CM'}) %> 3 | 4 |
5 | <%- include('partials/header') %> 6 |
7 |
8 |
9 |
10 |
Oops 😬
11 |
12 |
Something went wrong while processing that last request:
13 |
<%- error %>
14 | <% if(locals.operatorDisplay !== undefined && locals.operatorDisplay !== 'Anonymous') { %> 15 |
Operated By: <%= operatorDisplay %>
16 | <% } %> 17 |
18 |
19 |
20 |
21 |
22 | <%- include('partials/footer') %> 23 |
24 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reddit Context Bot", 3 | "description": "An event-based, reddit moderation bot built on top of snoowrap and written in typescript", 4 | "repository": "https://github.com/FoxxMD/context-mod", 5 | "stack": "container", 6 | "env": { 7 | "CLIENT_ID": { 8 | "description": "Client ID for your Reddit application", 9 | "value": "", 10 | "required": true 11 | }, 12 | "CLIENT_SECRET": { 13 | "description": "Client Secret for your Reddit application", 14 | "value": "", 15 | "required": true 16 | }, 17 | "REFRESH_TOKEN": { 18 | "description": "Refresh token retrieved from authenticating an account with your Reddit Application", 19 | "value": "", 20 | "required": false 21 | }, 22 | "ACCESS_TOKEN": { 23 | "description": "Access token retrieved from authenticating an account with your Reddit Application", 24 | "value": "", 25 | "required": false 26 | }, 27 | "REDIRECT_URI": { 28 | "description": "Redirect URI you specified when creating your Reddit Application. Required if you want to use the web interface. In the provided example replace 'your-heroku-app-name' with the name of your HEROKU app.", 29 | "value": "https://your-heroku-6app-name.herokuapp.com/callback", 30 | "required": false 31 | }, 32 | "OPERATOR": { 33 | "description": "Your reddit username WITHOUT any prefixes EXAMPLE /u/FoxxMD => FoxxMD. Specified user will be recognized as an admin.", 34 | "value": "", 35 | "required": false 36 | }, 37 | "WIKI_CONFIG": { 38 | "description": "Relative url to contextbot wiki page EX https://reddit.com/r/subreddit/wiki/", 39 | "value": "botconfig/contextbot", 40 | "required": false 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/3553.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[3553],{3553:(e,s,o)=>{o.r(s),o.d(s,{conf:()=>t,language:()=>n});var t={comments:{lineComment:"REM"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],folding:{markers:{start:new RegExp("^\\s*(::\\s*|REM\\s+)#region"),end:new RegExp("^\\s*(::\\s*|REM\\s+)#endregion")}}},n={defaultToken:"",ignoreCase:!0,tokenPostfix:".bat",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:/call|defined|echo|errorlevel|exist|for|goto|if|pause|set|shift|start|title|not|pushd|popd/,symbols:/[=>10% of their submissions", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "attr10sub", 13 | "kind": "attribution", 14 | // criteria defaults to OR -- so either of these criteria will trigger the rule 15 | "criteria": [ 16 | { 17 | // threshold can be a percent or an absolute number 18 | "threshold": "> 10%", 19 | // calculate percentage of submissions, rather than entire history (submissions & comments) 20 | "thresholdOn": "submissions", 21 | 22 | // look at last 90 days of Author's activities (comments and submissions) 23 | "window": "90 days" 24 | }, 25 | { 26 | "threshold": "> 10%", 27 | "thresholdOn": "submissions", 28 | // look at Author's last 100 activities (comments and submissions) 29 | "window": 100 30 | } 31 | ], 32 | } 33 | ], 34 | "actions": [ 35 | { 36 | "kind": "report", 37 | "content": "{{rules.attr10sub.largestPercent}}% of {{rules.attr10sub.activityTotal}} items over {{rules.attr10sub.window}}" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/1585.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[1585],{1585:(e,s,o)=>{o.r(s),o.d(s,{conf:()=>n,language:()=>l});var n={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},l={defaultToken:"",tokenPostfix:".dockerfile",variable:/\${?[\w]+}?/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/(ONBUILD)(\s+)/,["keyword",""]],[/(ENV)(\s+)([\w]+)/,["keyword","",{token:"variable",next:"@arguments"}]],[/(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT)/,{token:"keyword",next:"@arguments"}]],arguments:[{include:"@whitespace"},{include:"@strings"},[/(@variable)/,{cases:{"@eos":{token:"variable",next:"@popall"},"@default":"variable"}}],[/\\/,{cases:{"@eos":"","@default":""}}],[/./,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],whitespace:[[/\s+/,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],comment:[[/(^#.*$)/,"comment","@popall"]],strings:[[/\\'$/,"","@popall"],[/\\'/,""],[/'$/,"string","@popall"],[/'/,"string","@stringBody"],[/"$/,"string","@popall"],[/"/,"string","@dblStringBody"]],stringBody:[[/[^\\\$']/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/'$/,"string","@popall"],[/'/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]],dblStringBody:[[/[^\\\$"]/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/"$/,"string","@popall"],[/"/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]]}}}}]); -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy Jekyll site to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Setup Ruby 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: '3.0' # Not needed with a .ruby-version file 34 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 35 | cache-version: 0 # Increment this number if you need to re-download cached gems 36 | - name: Setup Pages 37 | id: pages 38 | uses: actions/configure-pages@v1 39 | - run: bundle exec jekyll build --baseurl ${{ steps.pages.outputs.base_path }} # defaults output to '/_site' 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v1 # This will automatically upload an artifact from the '/_site' directory 42 | 43 | # Deployment job 44 | deploy: 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | needs: build 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v1 54 | -------------------------------------------------------------------------------- /src/Common/Entities/ActivityReport.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Entity, 3 | Column, 4 | ManyToOne, JoinColumn, AfterLoad, 5 | } from "typeorm"; 6 | import {Activity} from "./Activity"; 7 | import {ManagerEntity} from "./ManagerEntity"; 8 | import {TimeAwareRandomBaseEntity} from "./Base/TimeAwareRandomBaseEntity"; 9 | import {Report, ReportType} from "../Infrastructure/Reddit"; 10 | 11 | @Entity() 12 | export class ActivityReport extends TimeAwareRandomBaseEntity { 13 | 14 | @Column({nullable: false, length: 500}) 15 | reason!: string 16 | 17 | @Column({nullable: false, length: 20}) 18 | type!: ReportType 19 | 20 | @Column({nullable: true, length: 100}) 21 | author?: string 22 | 23 | @Column("int", {nullable: false}) 24 | granularity: number = 0; 25 | 26 | @ManyToOne(type => Activity, act => act.reports, {cascade: ['update']}) 27 | @JoinColumn({name: 'activityId'}) 28 | activity!: Activity; 29 | 30 | @Column({nullable: false, name: 'activityId'}) 31 | activityId!: string 32 | 33 | constructor(data?: Report & { activity: Activity, granularity: number }) { 34 | super(); 35 | if (data !== undefined) { 36 | this.reason = data.reason; 37 | this.type = data.type; 38 | this.author = data.author; 39 | this.activity = data.activity; 40 | this.activityId = data.activity.id; 41 | this.granularity = data.granularity 42 | } 43 | } 44 | 45 | matchReport(report: Report): boolean { 46 | return this.reason === report.reason 47 | && this.type === report.type 48 | && this.author === report.author; 49 | } 50 | 51 | @AfterLoad() 52 | convertPrimitives() { 53 | if(this.author === null) { 54 | this.author = undefined; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Subreddit/ModNotes/ModAction.ts: -------------------------------------------------------------------------------- 1 | import {Submission, RedditUser, Comment, Subreddit, PrivateMessage} from "snoowrap/dist/objects" 2 | import {generateSnoowrapEntityFromRedditThing, parseRedditFullname} from "../../util" 3 | import Snoowrap from "snoowrap"; 4 | 5 | //import {ExtendedSnoowrap} from "../../Utils/SnoowrapClients"; 6 | 7 | export interface ModActionRaw { 8 | action?: string | null 9 | reddit_id?: string | null 10 | details?: string | null 11 | description?: string | null 12 | } 13 | 14 | export class ModAction { 15 | action?: string 16 | actedOn?: RedditUser | Submission | Comment | Subreddit | PrivateMessage 17 | details?: string 18 | description?: string 19 | 20 | constructor(data: ModActionRaw | undefined, client: Snoowrap) { 21 | const { 22 | action, 23 | reddit_id, 24 | details, 25 | description 26 | } = data || {}; 27 | this.action = action !== null ? action : undefined; 28 | this.details = details !== null ? details : undefined; 29 | this.description = description !== null ? description : undefined; 30 | 31 | if (reddit_id !== null && reddit_id !== undefined) { 32 | const thing = parseRedditFullname(reddit_id); 33 | if (thing !== undefined) { 34 | this.actedOn = generateSnoowrapEntityFromRedditThing(thing, client); 35 | } 36 | } 37 | } 38 | 39 | toRaw(): ModActionRaw { 40 | return { 41 | action: this.action, 42 | details: this.details, 43 | reddit_id: this.actedOn !== undefined ? this.actedOn.id : undefined, 44 | description: this.description 45 | } 46 | } 47 | 48 | toJSON() { 49 | return this.toRaw(); 50 | } 51 | } 52 | 53 | export default ModAction; 54 | -------------------------------------------------------------------------------- /src/Common/Migrations/Database/Server/1658930394548-Guests.ts: -------------------------------------------------------------------------------- 1 | import {MigrationInterface, QueryRunner, Table} from "typeorm" 2 | import {createdAtColumn, createdAtIndex, idIndex, index, randomIdColumn, timeAtColumn} from "../MigrationUtil"; 3 | 4 | export class Guests1658930394548 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const dbType = queryRunner.connection.driver.options.type; 8 | 9 | await queryRunner.createTable( 10 | new Table({ 11 | name: 'Guests', 12 | columns: [ 13 | randomIdColumn(), 14 | { 15 | name: 'authorName', 16 | type: 'varchar', 17 | length: '200', 18 | isNullable: false, 19 | }, 20 | { 21 | name: 'type', 22 | type: 'varchar', 23 | isNullable: false, 24 | length: '50' 25 | }, 26 | { 27 | name: 'guestOfId', 28 | type: 'varchar', 29 | length: '20', 30 | isNullable: true 31 | }, 32 | timeAtColumn('expiresAt', dbType, true), 33 | createdAtColumn(dbType), 34 | ], 35 | indices: [ 36 | idIndex('Guests', true), 37 | createdAtIndex('guests'), 38 | index('guest', ['expiresAt'], false) 39 | ] 40 | }), 41 | true, 42 | true, 43 | true 44 | ); 45 | } 46 | 47 | public async down(queryRunner: QueryRunner): Promise { 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Web/assets/views/noAccess.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%- include('partials/head', {title: 'Access Denied'}) %> 3 | 4 |
5 | <%- include('partials/title', {title: ''}) %> 6 |
7 |
8 |
9 |
10 |
Sorry!
11 |
12 |
Your account was successfully logged in but you cannot access this ContextMod client or route for one of these reasons:
13 |
    14 |
  • You are not a moderator of any of the subreddits this client has access to
  • 15 |
  • You are not a moderator of any of the subreddits of the specific instance/bot you are trying to access
  • 16 |
  • The instance(s) running the subreddits you are a moderator of are offline
  • 17 |
  • The bot(s) running the subreddits you are a moderator of are not configured correctly IE the subreddits they moderate could not be retrieved
  • 18 |
19 |
Note: You must Logout in order for the instance to detect changes in your subreddits/moderator status
20 | <% if(locals.operatorDisplay !== undefined && locals.operatorDisplay !== 'Anonymous') { %> 21 |
Operated By: <%= operatorDisplay %>
22 | <% } %> 23 |
24 |
25 |
26 |
27 |
28 | <%- include('partials/footer') %> 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/userNotes/usernoteFilter.json5: -------------------------------------------------------------------------------- 1 | { 2 | "runs": [ 3 | { 4 | "checks": [ 5 | { 6 | "name": "Self Promo Activities", 7 | "description": "Tag SP only if user does not have good contributor user note", 8 | // check will run on a new submission in your subreddit and look at the Author of that submission 9 | "kind": "submission", 10 | "rules": [ 11 | { 12 | "name": "attr10all", 13 | "kind": "attribution", 14 | "author": { 15 | "exclude": [ 16 | { 17 | // the key of the usernote type to look for https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 18 | // rule will not run if current usernote on Author is of type 'gooduser' 19 | "type": "gooduser" 20 | } 21 | ] 22 | }, 23 | "criteria": [ 24 | { 25 | "threshold": "> 10%", 26 | "window": "90 days" 27 | }, 28 | { 29 | "threshold": "> 10%", 30 | "window": 100 31 | } 32 | ], 33 | } 34 | ], 35 | "actions": [ 36 | { 37 | "kind": "usernote", 38 | // the key of usernote type 39 | // https://github.com/toolbox-team/reddit-moderator-toolbox/wiki/Subreddit-Wikis%3A-usernotes#working-with-note-types 40 | "type": "spamwarn", 41 | // content is mustache templated as usual 42 | "content": "Self Promotion: {{rules.attr10all.titlesDelim}} {{rules.attr10sub.largestPercent}}%" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /docs/subreddit-configuration/in-depth/author/ignoreVettedUser.yaml: -------------------------------------------------------------------------------- 1 | runs: 2 | - checks: 3 | - name: non-vetted karma/meme activity 4 | description: >- 5 | Report if Author has SP and has recent karma/meme sub activity and isn't 6 | vetted 7 | # check will run on a new submission in your subreddit and look at the Author of that submission 8 | kind: submission 9 | rules: 10 | # The Author Rule is best used in conjunction with other Rules -- 11 | # instead of having to write an AuthorFilter for every Rule where you want to skip it based on Author criteria 12 | # you can write one Author Rule and make it fail on the required criteria 13 | # so that the check fails and Actions don't run 14 | - name: nonvet 15 | kind: author 16 | exclude: 17 | - flairText: 18 | - vet 19 | - name: attr10 20 | kind: attribution 21 | criteria: 22 | - threshold: '> 10%' 23 | window: 90 days 24 | - threshold: '> 10%' 25 | window: 100 26 | - name: freekarma 27 | kind: recentActivity 28 | lookAt: submissions 29 | thresholds: 30 | - threshold: '>= 1' 31 | subreddits: 32 | - DeFreeKarma 33 | - FreeKarma4U 34 | window: 7 days 35 | - name: memes 36 | kind: recentActivity 37 | lookAt: submissions 38 | thresholds: 39 | - threshold: '>= 3' 40 | subreddits: 41 | - dankmemes 42 | window: 7 days 43 | # will NOT run if the Author for this Submission has the flair "vet" 44 | actions: 45 | - kind: report 46 | content: Author has posted in free karma or meme subs recently 47 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/3315.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[3315],{3315:(e,o,n)=>{n.r(o),n.d(o,{conf:()=>t,language:()=>s});var t={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"],["<",">"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"}]},s={defaultToken:"",tokenPostfix:".pascaligo",ignoreCase:!0,brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],keywords:["begin","block","case","const","else","end","fail","for","from","function","if","is","nil","of","remove","return","skip","then","type","var","while","with","option","None","transaction"],typeKeywords:["bool","int","list","map","nat","record","string","unit","address","map","mtz","xtz"],operators:["=",">","<","<=",">=","<>",":",":=","and","mod","or","+","-","*","/","@","&","^","%"],symbols:/[=><:@\^&|+\-*\/\^%]+/,tokenizer:{root:[[/[a-zA-Z_][\w]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/\$[0-9a-fA-F]{1,16}/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'/,"string","@string"],[/'[^\\']'/,"string"],[/'/,"string.invalid"],[/\#\d+/,"string"]],comment:[[/[^\(\*]+/,"comment"],[/\*\)/,"comment","@pop"],[/\(\*/,"comment"]],string:[[/[^\\']+/,"string"],[/\\./,"string.escape.invalid"],[/'/,{token:"string.quote",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,"white"],[/\(\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}}}}]); -------------------------------------------------------------------------------- /docs/subreddit-configuration/cookbook/youtubeCommentRepost.yaml: -------------------------------------------------------------------------------- 1 | #polling: 2 | # - newComm 3 | #runs: 4 | # - checks: 5 | #### Uncomment the code above to use this as a FULL subreddit config 6 | #### 7 | #### Otherwise copy-paste the code below to use as a CHECK 8 | # 9 | # If submission type is a youtube video CM will check top comments on the video and remove comment if it at least 85% the same 10 | # optionally, bans user if they have more than one modnote for comment reposts 11 | # 12 | - name: commRepostYT 13 | description: Check if comment has been reposted from youtube 14 | kind: comment 15 | itemIs: 16 | - removed: false 17 | approved: false 18 | op: false 19 | condition: AND 20 | rules: 21 | - name: commRepost 22 | kind: repost 23 | criteria: 24 | - searchOn: 25 | - external 26 | actions: 27 | - kind: remove 28 | spam: true 29 | note: 'reposted comment from youtube with {{rules.commrepostyt.closestSameness}}% sameness' 30 | 31 | - kind: ban 32 | authorIs: 33 | # if the author has more than one spamwatch usernote then just ban em 34 | include: 35 | - modActions: 36 | - noteType: SPAM_WATCH 37 | note: "/comment repost.*/i" 38 | search: total 39 | count: "> 1" 40 | message: You have been banned for repeated spammy behavior including reposting youtube comments 41 | note: yt comment repost + spammy behavior 42 | reason: yt comment repost + spammy behavior 43 | 44 | - name: commRepostYTModNote 45 | kind: modnote 46 | content: 'YT comment repost with {{rules.commrepostyt.closestSameness}}% sameness' 47 | type: SPAM_WATCH 48 | -------------------------------------------------------------------------------- /src/Web/assets/views/partials/subredditsTab.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if(locals.bots !== undefined) { %> 4 | <% bots.forEach(function (botData){ %> 5 | 29 | <% }) %> 30 | <% } %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/Common/Infrastructure/Runnable.ts: -------------------------------------------------------------------------------- 1 | import {MinimalOrFullFilter, MinimalOrFullFilterJson} from "./Filters/FilterShapes"; 2 | import {AuthorCriteria, TypedActivityState} from "./Filters/FilterCriteria"; 3 | import {Logger} from "winston"; 4 | import {SubredditResources} from "../../Subreddit/SubredditResources"; 5 | 6 | export interface RunnableBaseOptions extends Omit { 7 | logger: Logger; 8 | resources: SubredditResources 9 | itemIs?: MinimalOrFullFilter 10 | authorIs?: MinimalOrFullFilter 11 | } 12 | 13 | export interface StructuredRunnableBase extends RunnableBaseJson { 14 | itemIs?: MinimalOrFullFilter 15 | authorIs?: MinimalOrFullFilter 16 | } 17 | 18 | export interface TypedStructuredRunnableBase extends TypedRunnableBaseData { 19 | itemIs?: MinimalOrFullFilter 20 | authorIs?: MinimalOrFullFilter 21 | } 22 | 23 | export interface RunnableBaseJson { 24 | /** 25 | * A list of criteria to test the state of the `Activity` against before running the check. 26 | * 27 | * If any set of criteria passes the Check will be run. If the criteria fails then the Check will fail. 28 | * 29 | * * @examples [[{"over_18": true, "removed': false}]] 30 | * 31 | * */ 32 | itemIs?: MinimalOrFullFilterJson 33 | 34 | /** 35 | * If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail. 36 | * */ 37 | authorIs?: MinimalOrFullFilterJson 38 | } 39 | 40 | export interface TypedRunnableBaseData extends RunnableBaseJson { 41 | /** 42 | * If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail. 43 | * */ 44 | authorIs?: MinimalOrFullFilterJson 45 | } 46 | -------------------------------------------------------------------------------- /src/Web/assets/public/yaml/7135.entry.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkdemo=self.webpackChunkdemo||[]).push([[7135],{7135:(e,t,n)=>{n.r(t),n.d(t,{conf:()=>a,language:()=>o});var i=n(2526),a={comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["<",">"]],autoClosingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],surroundingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],onEnterRules:[{beforeText:new RegExp("<([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:i.Mj.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:i.Mj.IndentAction.Indent}}]},o={defaultToken:"",tokenPostfix:".xml",ignoreCase:!0,qualifiedName:/(?:[\w\.\-]+:)?[\w\.\-]+/,tokenizer:{root:[[/[^<&]+/,""],{include:"@whitespace"},[/(<)(@qualifiedName)/,[{token:"delimiter"},{token:"tag",next:"@tag"}]],[/(<\/)(@qualifiedName)(\s*)(>)/,[{token:"delimiter"},{token:"tag"},"",{token:"delimiter"}]],[/(<\?)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/(<\!)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/<\!\[CDATA\[/,{token:"delimiter.cdata",next:"@cdata"}],[/&\w+;/,"string.escape"]],cdata:[[/[^\]]+/,""],[/\]\]>/,{token:"delimiter.cdata",next:"@pop"}],[/\]/,""]],tag:[[/[ \t\r\n]+/,""],[/(@qualifiedName)(\s*=\s*)("[^"]*"|'[^']*')/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">?\/]*|'[^'>?\/]*)(?=[\?\/]\>)/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">]*|'[^'>]*)/,["attribute.name","","attribute.value"]],[/@qualifiedName/,"attribute.name"],[/\?>/,{token:"delimiter",next:"@pop"}],[/(\/)(>)/,[{token:"tag"},{token:"delimiter",next:"@pop"}]],[/>/,{token:"delimiter",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[//,{token:"comment",next:"@pop"}],[/