mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-01-14 07:57:57 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2d945db2c | ||
|
|
c5018183e0 | ||
|
|
c5358f196d | ||
|
|
1d9f8245f9 | ||
|
|
20b37f3a40 |
67
cliff.toml
Normal file
67
cliff.toml
Normal file
@@ -0,0 +1,67 @@
|
||||
# configuration file for git-cliff (0.1.0)
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}
|
||||
{%- if commit.breaking %}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(No Category)* {{ commit.message | upper_first }}
|
||||
{% if commit.breaking -%}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}
|
||||
"""
|
||||
# remove the leading and trailing whitespaces from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# allow only conventional commits
|
||||
# https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^doc", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "Security"},
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
@@ -99,6 +99,13 @@ export interface AuthorCriteria {
|
||||
* Does Author's account have a verified email?
|
||||
* */
|
||||
verified?: boolean
|
||||
|
||||
/**
|
||||
* Is the author shadowbanned?
|
||||
*
|
||||
* This is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned
|
||||
* */
|
||||
shadowBanned?: boolean
|
||||
}
|
||||
|
||||
export class Author implements AuthorCriteria {
|
||||
@@ -112,6 +119,7 @@ export class Author implements AuthorCriteria {
|
||||
linkKarma?: string;
|
||||
totalKarma?: string;
|
||||
verified?: boolean;
|
||||
shadowBanned?: boolean;
|
||||
|
||||
constructor(options: AuthorCriteria) {
|
||||
this.name = options.name;
|
||||
@@ -123,6 +131,7 @@ export class Author implements AuthorCriteria {
|
||||
this.commentKarma = options.commentKarma;
|
||||
this.linkKarma = options.linkKarma;
|
||||
this.totalKarma = options.totalKarma;
|
||||
this.shadowBanned = options.shadowBanned;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {MESSAGE} from 'triple-beam';
|
||||
import Poll from "snoostorm/out/util/Poll";
|
||||
import Snoowrap from "snoowrap";
|
||||
import {RuleResult} from "../Rule";
|
||||
import {IncomingMessage} from "http";
|
||||
|
||||
/**
|
||||
* An ISO 8601 Duration
|
||||
@@ -1547,3 +1548,11 @@ export interface RedditEntity {
|
||||
name: string
|
||||
type: RedditEntityType
|
||||
}
|
||||
|
||||
export interface StatusCodeError extends Error {
|
||||
name: 'StatusCodeError',
|
||||
statusCode: number,
|
||||
message: string,
|
||||
response: IncomingMessage,
|
||||
error: Error
|
||||
}
|
||||
|
||||
@@ -99,15 +99,16 @@ export interface AttributionCriteria {
|
||||
* This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`
|
||||
*
|
||||
* * If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)
|
||||
* * If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or reddit image/video (i.redd.it / v.redd.it)
|
||||
* * If `link` is included then aggregate author's submission history which is external links but not media
|
||||
* * If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)
|
||||
* * If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`
|
||||
* * If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit
|
||||
*
|
||||
* If nothing is specified or list is empty (default) all domains are aggregated
|
||||
*
|
||||
* @default undefined
|
||||
* @examples [[]]
|
||||
* */
|
||||
aggregateOn?: ('media' | 'self' | 'link')[],
|
||||
aggregateOn?: ('media' | 'redditMedia' | 'self' | 'link')[],
|
||||
|
||||
/**
|
||||
* Should the criteria consolidate recognized media domains into the parent domain?
|
||||
@@ -232,16 +233,23 @@ export class AttributionRule extends Rule {
|
||||
const domainInfo = getAttributionIdentifier(sub, consolidateMediaDomains)
|
||||
|
||||
let domainType = 'link';
|
||||
if(sub.secure_media !== undefined && sub.secure_media !== null) {
|
||||
domainType = 'media';
|
||||
} else if((sub.is_self || sub.is_video || sub.domain === 'i.redd.it')) {
|
||||
if(sub.is_video || ['i.redd.it','v.redd.it'].includes(sub.domain)
|
||||
// @ts-ignore
|
||||
|| sub.gallery_data !== undefined) {
|
||||
domainType = 'redditMedia';
|
||||
} else if(sub.is_self || sub.domain === 'reddit.com') {
|
||||
domainType = 'self';
|
||||
} else if(sub.secure_media !== undefined && sub.secure_media !== null) {
|
||||
domainType = 'media';
|
||||
}
|
||||
|
||||
if(aggregateOn.length !== 0) {
|
||||
if(domainType === 'media' && !aggregateOn.includes('media')) {
|
||||
return acc;
|
||||
}
|
||||
if(domainType === 'redditMedia' && !aggregateOn.includes('redditMedia')) {
|
||||
return acc;
|
||||
}
|
||||
if(domainType === 'self' && !aggregateOn.includes('self')) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shadowBanned": {
|
||||
"description": "Is the author shadowbanned?\n\nThis is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totalKarma": {
|
||||
"description": "A string containing a comparison operator and a value to compare against\n\nThe syntax is `(< OR > OR <= OR >=) <number>`\n\n* EX `> 100` => greater than 100",
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or reddit image/video (i.redd.it / v.redd.it)\n* If `link` is included then aggregate author's submission history which is external links but not media\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -242,6 +242,7 @@
|
||||
"enum": [
|
||||
"link",
|
||||
"media",
|
||||
"redditMedia",
|
||||
"self"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -498,6 +499,10 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shadowBanned": {
|
||||
"description": "Is the author shadowbanned?\n\nThis is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totalKarma": {
|
||||
"description": "A string containing a comparison operator and a value to compare against\n\nThe syntax is `(< OR > OR <= OR >=) <number>`\n\n* EX `> 100` => greater than 100",
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
|
||||
@@ -179,7 +179,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or reddit image/video (i.redd.it / v.redd.it)\n* If `link` is included then aggregate author's submission history which is external links but not media\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -188,6 +188,7 @@
|
||||
"enum": [
|
||||
"link",
|
||||
"media",
|
||||
"redditMedia",
|
||||
"self"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -444,6 +445,10 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shadowBanned": {
|
||||
"description": "Is the author shadowbanned?\n\nThis is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totalKarma": {
|
||||
"description": "A string containing a comparison operator and a value to compare against\n\nThe syntax is `(< OR > OR <= OR >=) <number>`\n\n* EX `> 100` => greater than 100",
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"properties": {
|
||||
"aggregateOn": {
|
||||
"default": "undefined",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or reddit image/video (i.redd.it / v.redd.it)\n* If `link` is included then aggregate author's submission history which is external links but not media\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"description": "This list determines which categories of domains should be aggregated on. All aggregated domains will be tested against `threshold`\n\n* If `media` is included then aggregate author's submission history which reddit recognizes as media (youtube, vimeo, etc.)\n* If `redditMedia` is included then aggregate on author's submissions history which are media hosted on reddit: galleries, videos, and images (i.redd.it / v.redd.it)\n* If `self` is included then aggregate on author's submission history which are self-post (`self.[subreddit]`) or domain is `reddit.com`\n* If `link` is included then aggregate author's submission history which is external links and not recognized as `media` by reddit\n\nIf nothing is specified or list is empty (default) all domains are aggregated",
|
||||
"examples": [
|
||||
[
|
||||
]
|
||||
@@ -165,6 +165,7 @@
|
||||
"enum": [
|
||||
"link",
|
||||
"media",
|
||||
"redditMedia",
|
||||
"self"
|
||||
],
|
||||
"type": "string"
|
||||
@@ -421,6 +422,10 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"shadowBanned": {
|
||||
"description": "Is the author shadowbanned?\n\nThis is determined by trying to retrieve the author's profile. If a 404 is returned it is likely they are shadowbanned",
|
||||
"type": "boolean"
|
||||
},
|
||||
"totalKarma": {
|
||||
"description": "A string containing a comparison operator and a value to compare against\n\nThe syntax is `(< OR > OR <= OR >=) <number>`\n\n* EX `> 100` => greater than 100",
|
||||
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
} from "../Common/interfaces";
|
||||
import {
|
||||
compareDurationValue,
|
||||
comparisonTextOp,
|
||||
isActivityWindowCriteria,
|
||||
comparisonTextOp, getActivityAuthorName,
|
||||
isActivityWindowCriteria, isStatusError,
|
||||
normalizeName,
|
||||
parseDuration,
|
||||
parseDurationComparison,
|
||||
@@ -125,17 +125,25 @@ export async function getAuthorActivities(user: RedditUser, options: AuthorTyped
|
||||
|
||||
let items: Array<Submission | Comment> = [];
|
||||
//let count = 1;
|
||||
let listing;
|
||||
switch (options.type) {
|
||||
case 'comment':
|
||||
listing = await user.getComments({limit: chunkSize});
|
||||
break;
|
||||
case 'submission':
|
||||
listing = await user.getSubmissions({limit: chunkSize});
|
||||
break;
|
||||
default:
|
||||
listing = await user.getOverview({limit: chunkSize});
|
||||
break;
|
||||
let listing = [];
|
||||
try {
|
||||
switch (options.type) {
|
||||
case 'comment':
|
||||
listing = await user.getComments({limit: chunkSize});
|
||||
break;
|
||||
case 'submission':
|
||||
listing = await user.getSubmissions({limit: chunkSize});
|
||||
break;
|
||||
default:
|
||||
listing = await user.getOverview({limit: chunkSize});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
if(isStatusError(err) && err.statusCode === 404) {
|
||||
throw new SimpleError('Reddit returned a 404 for user history. Likely this user is shadowbanned.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
let hitEnd = false;
|
||||
let offset = chunkSize;
|
||||
@@ -317,179 +325,211 @@ export const renderContent = async (template: string, data: (Submission | Commen
|
||||
}
|
||||
|
||||
export const testAuthorCriteria = async (item: (Comment | Submission), authorOpts: AuthorCriteria, include = true, userNotes: UserNotes) => {
|
||||
// @ts-ignore
|
||||
const author: RedditUser = await item.author;
|
||||
for (const k of Object.keys(authorOpts)) {
|
||||
// @ts-ignore
|
||||
if (authorOpts[k] !== undefined) {
|
||||
switch (k) {
|
||||
case 'name':
|
||||
const authPass = () => {
|
||||
// @ts-ignore
|
||||
for (const n of authorOpts[k]) {
|
||||
if (n.toLowerCase() === author.name.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const authResult = authPass();
|
||||
if ((include && !authResult) || (!include && authResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'flairCssClass':
|
||||
const css = await item.author_flair_css_class;
|
||||
const cssPass = () => {
|
||||
// @ts-ignore
|
||||
for (const c of authorOpts[k]) {
|
||||
if (c === css) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const cssResult = cssPass();
|
||||
if ((include && !cssResult) || (!include && cssResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'flairText':
|
||||
const text = await item.author_flair_text;
|
||||
const textPass = () => {
|
||||
// @ts-ignore
|
||||
for (const c of authorOpts[k]) {
|
||||
if (c === text) {
|
||||
return
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const textResult = textPass();
|
||||
if ((include && !textResult) || (!include && textResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'isMod':
|
||||
const mods: RedditUser[] = await item.subreddit.getModerators();
|
||||
const isModerator = mods.some(x => x.name === item.author.name);
|
||||
const modMatch = authorOpts.isMod === isModerator;
|
||||
if ((include && !modMatch) || (!include && modMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'age':
|
||||
const ageTest = compareDurationValue(parseDurationComparison(await authorOpts.age as string), dayjs.unix(await item.author.created));
|
||||
if ((include && !ageTest) || (!include && ageTest)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'linkKarma':
|
||||
const lkCompare = parseGenericValueOrPercentComparison(await authorOpts.linkKarma as string);
|
||||
let lkMatch;
|
||||
if (lkCompare.isPercent) {
|
||||
// @ts-ignore
|
||||
const tk = author.total_karma as number;
|
||||
lkMatch = comparisonTextOp(author.link_karma / tk, lkCompare.operator, lkCompare.value / 100);
|
||||
} else {
|
||||
lkMatch = comparisonTextOp(author.link_karma, lkCompare.operator, lkCompare.value);
|
||||
}
|
||||
if ((include && !lkMatch) || (!include && lkMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'commentKarma':
|
||||
const ckCompare = parseGenericValueOrPercentComparison(await authorOpts.commentKarma as string);
|
||||
let ckMatch;
|
||||
if (ckCompare.isPercent) {
|
||||
// @ts-ignore
|
||||
const ck = author.total_karma as number;
|
||||
ckMatch = comparisonTextOp(author.comment_karma / ck, ckCompare.operator, ckCompare.value / 100);
|
||||
} else {
|
||||
ckMatch = comparisonTextOp(author.comment_karma, ckCompare.operator, ckCompare.value);
|
||||
}
|
||||
if ((include && !ckMatch) || (!include && ckMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'totalKarma':
|
||||
const tkCompare = parseGenericValueComparison(await authorOpts.totalKarma as string);
|
||||
if (tkCompare.isPercent) {
|
||||
throw new SimpleError(`'totalKarma' value on AuthorCriteria cannot be a percentage`);
|
||||
}
|
||||
// @ts-ignore
|
||||
const totalKarma = author.total_karma as number;
|
||||
const tkMatch = comparisonTextOp(totalKarma, tkCompare.operator, tkCompare.value);
|
||||
if ((include && !tkMatch) || (!include && tkMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'verified':
|
||||
const vMatch = await author.has_verified_mail === authorOpts.verified as boolean;
|
||||
if ((include && !vMatch) || (!include && vMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'userNotes':
|
||||
const notes = await userNotes.getUserNotes(item.author);
|
||||
const notePass = () => {
|
||||
for (const noteCriteria of authorOpts[k] as UserNoteCriteria[]) {
|
||||
const {count = '>= 1', search = 'current', type} = noteCriteria;
|
||||
const {
|
||||
value,
|
||||
operator,
|
||||
isPercent,
|
||||
extra = ''
|
||||
} = parseGenericValueOrPercentComparison(count);
|
||||
const order = extra.includes('asc') ? 'ascending' : 'descending';
|
||||
switch (search) {
|
||||
case 'current':
|
||||
if (notes.length > 0 && notes[notes.length - 1].noteType === type) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'consecutive':
|
||||
let orderedNotes = notes;
|
||||
if (order === 'descending') {
|
||||
orderedNotes = [...notes];
|
||||
orderedNotes.reverse();
|
||||
}
|
||||
let currCount = 0;
|
||||
for (const note of orderedNotes) {
|
||||
if (note.noteType === type) {
|
||||
currCount++;
|
||||
} else {
|
||||
currCount = 0;
|
||||
}
|
||||
if (isPercent) {
|
||||
throw new SimpleError(`When comparing UserNotes with 'consecutive' search 'count' cannot be a percentage. Given: ${count}`);
|
||||
}
|
||||
if (comparisonTextOp(currCount, operator, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'total':
|
||||
if (isPercent) {
|
||||
if (comparisonTextOp(notes.filter(x => x.noteType === type).length / notes.length, operator, value / 100)) {
|
||||
return true;
|
||||
}
|
||||
} else if (comparisonTextOp(notes.filter(x => x.noteType === type).length, operator, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const noteResult = notePass();
|
||||
if ((include && !noteResult) || (!include && noteResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
const {shadowBanned, ...rest} = authorOpts;
|
||||
|
||||
if(shadowBanned !== undefined) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
await item.author.fetch();
|
||||
// user is not shadowbanned
|
||||
// if criteria specifies they SHOULD be shadowbanned then return false now
|
||||
if(shadowBanned) {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
if(isStatusError(err) && err.statusCode === 404) {
|
||||
// user is shadowbanned
|
||||
// if criteria specifies they should not be shadowbanned then return false now
|
||||
if(!shadowBanned) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
try {
|
||||
const authorName = getActivityAuthorName(item.author);
|
||||
|
||||
for (const k of Object.keys(rest)) {
|
||||
// @ts-ignore
|
||||
if (authorOpts[k] !== undefined) {
|
||||
switch (k) {
|
||||
case 'name':
|
||||
const authPass = () => {
|
||||
// @ts-ignore
|
||||
for (const n of authorOpts[k]) {
|
||||
if (n.toLowerCase() === authorName.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const authResult = authPass();
|
||||
if ((include && !authResult) || (!include && authResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'flairCssClass':
|
||||
const css = await item.author_flair_css_class;
|
||||
const cssPass = () => {
|
||||
// @ts-ignore
|
||||
for (const c of authorOpts[k]) {
|
||||
if (c === css) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const cssResult = cssPass();
|
||||
if ((include && !cssResult) || (!include && cssResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'flairText':
|
||||
const text = await item.author_flair_text;
|
||||
const textPass = () => {
|
||||
// @ts-ignore
|
||||
for (const c of authorOpts[k]) {
|
||||
if (c === text) {
|
||||
return
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const textResult = textPass();
|
||||
if ((include && !textResult) || (!include && textResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'isMod':
|
||||
const mods: RedditUser[] = await item.subreddit.getModerators();
|
||||
const isModerator = mods.some(x => x.name === authorName);
|
||||
const modMatch = authorOpts.isMod === isModerator;
|
||||
if ((include && !modMatch) || (!include && modMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'age':
|
||||
const ageTest = compareDurationValue(parseDurationComparison(await authorOpts.age as string), dayjs.unix(await item.author.created));
|
||||
if ((include && !ageTest) || (!include && ageTest)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'linkKarma':
|
||||
const lkCompare = parseGenericValueOrPercentComparison(await authorOpts.linkKarma as string);
|
||||
let lkMatch;
|
||||
if (lkCompare.isPercent) {
|
||||
// @ts-ignore
|
||||
const tk = await item.author.total_karma as number;
|
||||
lkMatch = comparisonTextOp(item.author.link_karma / tk, lkCompare.operator, lkCompare.value / 100);
|
||||
} else {
|
||||
lkMatch = comparisonTextOp(item.author.link_karma, lkCompare.operator, lkCompare.value);
|
||||
}
|
||||
if ((include && !lkMatch) || (!include && lkMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'commentKarma':
|
||||
const ckCompare = parseGenericValueOrPercentComparison(await authorOpts.commentKarma as string);
|
||||
let ckMatch;
|
||||
if (ckCompare.isPercent) {
|
||||
// @ts-ignore
|
||||
const ck = await item.author.total_karma as number;
|
||||
ckMatch = comparisonTextOp(item.author.comment_karma / ck, ckCompare.operator, ckCompare.value / 100);
|
||||
} else {
|
||||
ckMatch = comparisonTextOp(item.author.comment_karma, ckCompare.operator, ckCompare.value);
|
||||
}
|
||||
if ((include && !ckMatch) || (!include && ckMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'totalKarma':
|
||||
const tkCompare = parseGenericValueComparison(await authorOpts.totalKarma as string);
|
||||
if (tkCompare.isPercent) {
|
||||
throw new SimpleError(`'totalKarma' value on AuthorCriteria cannot be a percentage`);
|
||||
}
|
||||
// @ts-ignore
|
||||
const totalKarma = await item.author.total_karma as number;
|
||||
const tkMatch = comparisonTextOp(totalKarma, tkCompare.operator, tkCompare.value);
|
||||
if ((include && !tkMatch) || (!include && tkMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'verified':
|
||||
const vMatch = await item.author.has_verified_mail === authorOpts.verified as boolean;
|
||||
if ((include && !vMatch) || (!include && vMatch)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'userNotes':
|
||||
const notes = await userNotes.getUserNotes(item.author);
|
||||
const notePass = () => {
|
||||
for (const noteCriteria of authorOpts[k] as UserNoteCriteria[]) {
|
||||
const {count = '>= 1', search = 'current', type} = noteCriteria;
|
||||
const {
|
||||
value,
|
||||
operator,
|
||||
isPercent,
|
||||
extra = ''
|
||||
} = parseGenericValueOrPercentComparison(count);
|
||||
const order = extra.includes('asc') ? 'ascending' : 'descending';
|
||||
switch (search) {
|
||||
case 'current':
|
||||
if (notes.length > 0 && notes[notes.length - 1].noteType === type) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'consecutive':
|
||||
let orderedNotes = notes;
|
||||
if (order === 'descending') {
|
||||
orderedNotes = [...notes];
|
||||
orderedNotes.reverse();
|
||||
}
|
||||
let currCount = 0;
|
||||
for (const note of orderedNotes) {
|
||||
if (note.noteType === type) {
|
||||
currCount++;
|
||||
} else {
|
||||
currCount = 0;
|
||||
}
|
||||
if (isPercent) {
|
||||
throw new SimpleError(`When comparing UserNotes with 'consecutive' search 'count' cannot be a percentage. Given: ${count}`);
|
||||
}
|
||||
if (comparisonTextOp(currCount, operator, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'total':
|
||||
if (isPercent) {
|
||||
if (comparisonTextOp(notes.filter(x => x.noteType === type).length / notes.length, operator, value / 100)) {
|
||||
return true;
|
||||
}
|
||||
} else if (comparisonTextOp(notes.filter(x => x.noteType === type).length, operator, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const noteResult = notePass();
|
||||
if ((include && !noteResult) || (!include && noteResult)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
if(isStatusError(err) && err.statusCode === 404) {
|
||||
throw new SimpleError('Reddit returned a 404 while trying to retrieve User profile. It is likely this user is shadowbanned.');
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ItemContent {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ActivityWindowCriteria, CacheOptions, CacheProvider,
|
||||
DurationComparison,
|
||||
GenericComparison, LogInfo, NamedGroup,
|
||||
PollingOptionsStrong, RedditEntity, RedditEntityType, RegExResult, ResourceStats,
|
||||
PollingOptionsStrong, RedditEntity, RedditEntityType, RegExResult, ResourceStats, StatusCodeError,
|
||||
StringOperator, StrongSubredditState, SubredditState
|
||||
} from "./Common/interfaces";
|
||||
import JSON5 from "json5";
|
||||
@@ -1075,6 +1075,10 @@ export const isScopeError = (err: any): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
export const isStatusError = (err: any): err is StatusCodeError => {
|
||||
return typeof err === 'object' && err.name === 'StatusCodeError' && err.response !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached activities lose type information when deserialized so need to check properties as well to see if the object is the shape of a Submission
|
||||
* */
|
||||
|
||||
Reference in New Issue
Block a user