Compare commits

...

5 Commits
0.9.0 ... 0.9.1

Author SHA1 Message Date
FoxxMD
d2d945db2c Merge branch 'edge' 2021-09-21 15:08:28 -04:00
FoxxMD
c5018183e0 fix(attribution): Improve parsing of domain type to fix bug with galleries
* Add `redditMedia` as distinct domain type from `self` for more granular aggregation
* Use `redditMedia` to fix bug where video and galleries were being counted as `media`
2021-09-20 16:34:29 -04:00
FoxxMD
c5358f196d feat(author): Handle shadowbanned users
* Allow checking if user is shadowbanned via authorIs (AuthorCriteria)
* try-catch on history get or author criteria to try to detect shadowbanned user for a more descriptive error
2021-09-20 13:49:35 -04:00
FoxxMD
1d9f8245f9 feat(tooling): scope-based sorting with BC note for git cliff generation 2021-09-20 11:51:25 -04:00
FoxxMD
20b37f3a40 Initial git cliff config 2021-09-20 11:03:37 -04:00
10 changed files with 349 additions and 193 deletions

67
cliff.toml Normal file
View 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"

View File

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

View File

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

View File

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

View File

@@ -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*(%?)(.*)$",

View File

@@ -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*(%?)(.*)$",

View File

@@ -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*(%?)(.*)$",

View File

@@ -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*(%?)(.*)$",

View File

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

View File

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