Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bot/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,11 +1002,12 @@ const userTakerIsBlockedByUserOrder = async (
const notMeetingRequirementsMessage = async (
ctx: MainContext,
user: UserDocument,
requirements?: { min_days_using_bot: number; min_completed_orders: number },
) => {
try {
await ctx.telegram.sendMessage(
user.tg_id,
ctx.i18n.t('not_meeting_requirements'),
ctx.i18n.t('not_meeting_requirements', requirements),
);
} catch (error) {
logger.error(error);
Expand Down
25 changes: 17 additions & 8 deletions bot/modules/orders/takeOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ export const takebuy = async (
if (await isBannedFromCommunity(user, order.community_id))
return await messages.bannedUserErrorMessage(ctx, user);

if (!(await meetsCounterpartyRequirements(ctx, user, userOffer))) return;

if (!(await validateTakeBuyOrder(ctx, bot, user, order))) return;

if (!(await meetsCounterpartyRequirements(ctx, user, userOffer))) return;

const { randomImage } = generateRandomImage(user._id.toString());

order.status = 'WAITING_PAYMENT';
Expand Down Expand Up @@ -138,9 +138,9 @@ export const takesell = async (
if (await isBannedFromCommunity(user, order.community_id))
return await messages.bannedUserErrorMessage(ctx, user);

if (!(await meetsCounterpartyRequirements(ctx, user, seller))) return;

if (!(await validateTakeSellOrder(ctx, bot, user, order))) return;

if (!(await meetsCounterpartyRequirements(ctx, user, seller))) return;
order.status = 'WAITING_BUYER_INVOICE';
order.buyer_id = user._id;
order.taken_at = new Date(Date.now());
Expand All @@ -158,7 +158,7 @@ export const takesell = async (
}
};

const meetsCounterpartyRequirements = async (
export const meetsCounterpartyRequirements = async (
ctx: MainContext,
user: UserDocument,
orderCreator: UserDocument,
Expand All @@ -170,15 +170,24 @@ const meetsCounterpartyRequirements = async (

if (min_days_using_bot > 0) {
const ageInDays = getUserAge(user);
if (ageInDays < min_days_using_bot) {
await messages.notMeetingRequirementsMessage(ctx, user);
// Legacy accounts without a created_at yield NaN here. Such accounts
// predate created_at tracking and are genuinely old, so we let them pass
// the age check explicitly instead of relying on NaN comparisons.
if (!Number.isNaN(ageInDays) && ageInDays < min_days_using_bot) {
await messages.notMeetingRequirementsMessage(ctx, user, {
min_days_using_bot,
min_completed_orders,
});
return false;
}
}

if (min_completed_orders > 0) {
if (user.trades_completed < min_completed_orders) {
await messages.notMeetingRequirementsMessage(ctx, user);
await messages.notMeetingRequirementsMessage(ctx, user, {
min_days_using_bot,
min_completed_orders,
});
return false;
}
}
Expand Down
133 changes: 60 additions & 73 deletions bot/modules/user/scenes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ const readNonNegativeInt = (value: string | undefined, fallback: number) => {
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
};

const DEFAULT_COUNTERPARTY_REQUIREMENTS = {
min_days_using_bot: 0,
min_completed_orders: 0,
};

// Fallback caps when the corresponding MAX_COUNTERPARTY_* env vars are unset,
// mirroring the values documented in .env-sample.
const DEFAULT_MAX_COUNTERPARTY_AGE = 30;
const DEFAULT_MAX_COUNTERPARTY_ORDERS = 10;

function make() {
const resetMessage = async (ctx: CommunityContext, next: () => void) => {
const state = ctx.scene.state as CommunityWizardState;
Expand Down Expand Up @@ -142,92 +152,52 @@ function make() {
}
});

scene.command(
'counterpartyage',
resetMessage,
async (ctx: CommunityContext) => {
// counterpartyage and counterpartyorders only differ in the field they set,
// the env cap, its fallback, and the feedback key/param, so we build both
// from a single factory.
const makeRequirementCommand = ({
command,
envVar,
fallbackMax,
field,
feedbackKey,
paramKey,
}: {
command: string;
envVar: string;
fallbackMax: number;
field: 'min_days_using_bot' | 'min_completed_orders';
feedbackKey: string;
paramKey: string;
}) => {
scene.command(command, resetMessage, async (ctx: CommunityContext) => {
try {
await ctx.deleteMessage();
const state = ctx.scene.state as CommunityWizardState;
if (ctx.message === undefined || !('text' in ctx.message))
throw new Error('ctx.message is undefined');
const [, days] = ctx.message.text.trim().split(' ');
const min_days = parseInt(days);
if (isNaN(min_days) || min_days < 0) throw new Error('NotValidNumber');
const maxAge = readNonNegativeInt(
process.env.MAX_COUNTERPARTY_AGE_REQUIREMENT,
30,
);
if (min_days > maxAge) {
state.error = {
i18n: 'invalid_range',
command: '/counterpartyage',
max: maxAge,
};
return await updateMessage(ctx);
}
const user = state.user;
if (!user.counterparty_requirements) {
user.counterparty_requirements = {
min_days_using_bot: 0,
min_completed_orders: 0,
};
}
user.counterparty_requirements.min_days_using_bot = min_days;
await user.save();
state.feedback = { i18n: 'counterpartyage_updated', days: min_days };
await updateMessage(ctx);
} catch (err) {
logger.error(err);
(ctx.scene.state as CommunityWizardState).error = {
i18n:
err instanceof Error && err.message === 'NotValidNumber'
? 'invalid_number'
: 'generic_error',
};
await updateMessage(ctx);
}
},
);

scene.command(
'counterpartyorders',
resetMessage,
async (ctx: CommunityContext) => {
try {
await ctx.deleteMessage();
const state = ctx.scene.state as CommunityWizardState;
if (ctx.message === undefined || !('text' in ctx.message))
throw new Error('ctx.message is undefined');
const [, orders] = ctx.message.text.trim().split(' ');
const min_orders = parseInt(orders);
if (isNaN(min_orders) || min_orders < 0)
const [, value] = ctx.message.text.trim().split(' ');
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0)
throw new Error('NotValidNumber');
const maxOrders = readNonNegativeInt(
process.env.MAX_COUNTERPARTY_ORDERS_REQUIREMENT,
10,
);
if (min_orders > maxOrders) {
const max = readNonNegativeInt(process.env[envVar], fallbackMax);
if (parsed > max) {
state.error = {
i18n: 'invalid_range',
command: '/counterpartyorders',
max: maxOrders,
command: '/' + command,
max,
};
return await updateMessage(ctx);
}
const user = state.user;
if (!user.counterparty_requirements) {
user.counterparty_requirements = {
min_days_using_bot: 0,
min_completed_orders: 0,
...DEFAULT_COUNTERPARTY_REQUIREMENTS,
};
}
user.counterparty_requirements.min_completed_orders = min_orders;
user.counterparty_requirements[field] = parsed;
await user.save();
state.feedback = {
i18n: 'counterpartyorders_updated',
orders: min_orders,
};
state.feedback = { i18n: feedbackKey, [paramKey]: parsed };
await updateMessage(ctx);
} catch (err) {
logger.error(err);
Expand All @@ -239,8 +209,26 @@ function make() {
};
await updateMessage(ctx);
}
},
);
});
};

makeRequirementCommand({
command: 'counterpartyage',
envVar: 'MAX_COUNTERPARTY_AGE_REQUIREMENT',
fallbackMax: DEFAULT_MAX_COUNTERPARTY_AGE,
field: 'min_days_using_bot',
feedbackKey: 'counterpartyage_updated',
paramKey: 'days',
});

makeRequirementCommand({
command: 'counterpartyorders',
envVar: 'MAX_COUNTERPARTY_ORDERS_REQUIREMENT',
fallbackMax: DEFAULT_MAX_COUNTERPARTY_ORDERS,
field: 'min_completed_orders',
feedbackKey: 'counterpartyorders_updated',
paramKey: 'orders',
});

scene.command(
'resetrequirements',
Expand All @@ -251,8 +239,7 @@ function make() {
const state = ctx.scene.state as CommunityWizardState;
const user = state.user;
user.counterparty_requirements = {
min_days_using_bot: 0,
min_completed_orders: 0,
...DEFAULT_COUNTERPARTY_REQUIREMENTS,
};
await user.save();
state.feedback = { i18n: 'requirements_reset' };
Expand Down
Loading
Loading