Files
ripunzel/src/sources/user.js
T

80 lines
3.2 KiB
JavaScript

'use strict';
const config = require('config');
const curateUser = require('../curate/user');
const logger = require('../logger')(__filename);
// reddit tells deleted and suspended accounts apart (verified live, 2026-11):
// - deleted: /user/x/about.json returns a JSON 404 (a shadowbanned account reportedly looks
// the same, so a 404 isn't necessarily permanent - see accountStatus.js's grace period)
// - suspended: about.json returns 200 with only { name, is_suspended: true, ... } - no date of
// suspension is exposed anywhere
// Anything else (network errors, 5xx, or the HTML bot-challenge page, which carries no
// `status` - see reddit/client.js's requestJson()) is 'error', which must never be treated as
// dead: a blocked or expired session would otherwise get the entire users file cleaned out.
async function fetchUser(username, reddit) {
try {
const user = await reddit.getUser(username);
if (user.is_suspended) {
return { status: 'suspended', user: null };
}
return { status: 'active', user };
} catch (error) {
if (error.status === 404) {
return { status: 'deleted', user: null };
}
logger.error(`Failed to fetch reddit user '${username}': ${error.message} (https://reddit.com/user/${username})`);
return { status: 'error', user: null, error };
}
}
async function getSocialLinks(username, reddit) {
try {
return await reddit.getSocialLinks(username);
} catch (error) {
// supplementary, not worth failing the whole user fetch over - fall back to none
logger.warn(`Failed to fetch social links for '${username}': ${error.message} (https://reddit.com/user/${username})`);
return [];
}
}
// social links are only ever used for the profile description file (save/profileDetails.js),
// which is skipped once it's in the index (unless --redownload-profile) - scraping the profile
// page for them regardless costs one throttled reddit request per user per run for nothing
function needsSocialLinks(indexedProfile, args) {
return Boolean(config.library.profile.description && (args.redownloadProfile || !indexedProfile?.description));
}
// the user as getIndex() needs it to resolve the index path - the full curated user (minus social
// links, which depend on the index) in case a custom library.index.file pattern uses more than
// the name, otherwise the same fallback as resolveUser() below
function indexUser(username, fetched) {
return fetched.status === 'active' ? curateUser(fetched.user) : { name: username, fallback: true };
}
// curated user, or the same minimal fallback as before for an account that couldn't be fetched
// (or is suspended/deleted, when the caller still wants to proceed - e.g. a --post by them)
async function resolveUser(username, fetched, indexedProfile, reddit, args) {
if (fetched.status !== 'active') {
return { name: username, fallback: true };
}
const socialLinks = fetched.user.subreddit && needsSocialLinks(indexedProfile, args)
? await getSocialLinks(username, reddit)
: [];
return curateUser(fetched.user, socialLinks);
}
module.exports = {
fetchUser,
indexUser,
resolveUser,
};