Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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: 3 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ PAYMENT_ATTEMPTS=2

# Here will go the disputes from non community orders
DISPUTE_CHANNEL='@p2plnbotDispute'
# Counterparty requirements limits
MAX_COUNTERPARTY_AGE_REQUIREMENT=30
MAX_COUNTERPARTY_ORDERS_REQUIREMENT=10

# time-to-live for communities in days, communities without successful orders on this time are deleted
COMMUNITY_TTL=31
Expand Down
15 changes: 15 additions & 0 deletions bot/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,20 @@ const userTakerIsBlockedByUserOrder = async (
}
};

const notMeetingRequirementsMessage = async (
ctx: MainContext,
user: UserDocument,
) => {
try {
await ctx.telegram.sendMessage(
user.tg_id,
ctx.i18n.t('not_meeting_requirements'),
);
} catch (error) {
logger.error(error);
}
};

const fiatSentMessages = async (
ctx: MainContext,
buyer: UserDocument,
Expand Down Expand Up @@ -2233,4 +2247,5 @@ export {
userTakerIsBlockedByUserOrder,
userOrderIsBlockedByUserTaker,
showQRCodeMessage,
notMeetingRequirementsMessage,
};
1 change: 1 addition & 0 deletions bot/middleware/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const stageMiddleware = () => {
addInvoicePHIWizard,
OrdersModule.Scenes.createOrder,
UserModule.Scenes.Settings,
UserModule.Scenes.Requirements,
];
scenes.forEach(addGenericCommands);
const stage = new Scenes.Stage(scenes, {
Expand Down
39 changes: 38 additions & 1 deletion bot/modules/orders/takeOrder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { logger } from '../../../logger';
import { Block, Order, User } from '../../../models';
import { UserDocument } from '../../../models/user';
import { deleteOrderFromChannel, generateRandomImage } from '../../../util';
import {
deleteOrderFromChannel,
generateRandomImage,
getUserAge,
} from '../../../util';
import * as messages from '../../messages';
import { HasTelegram, MainContext } from '../../start';
import {
Expand Down Expand Up @@ -73,6 +77,8 @@ 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;

const { randomImage, isGoldenHoneyBadger } = await generateRandomImage(
Expand Down Expand Up @@ -130,6 +136,9 @@ export const takesell = async (
// We verify if the user is not banned on this community
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;
order.status = 'WAITING_BUYER_INVOICE';
order.buyer_id = user._id;
Expand Down Expand Up @@ -171,3 +180,31 @@ const checkBlockingStatus = async (

return false;
};

const meetsCounterpartyRequirements = async (
ctx: MainContext,
user: UserDocument,
orderCreator: UserDocument,
) => {
if (!orderCreator.counterparty_requirements) return true;

const { min_days_using_bot, min_completed_orders } =
orderCreator.counterparty_requirements;

if (min_days_using_bot > 0) {
const ageInDays = getUserAge(user);
if (ageInDays < min_days_using_bot) {
await messages.notMeetingRequirementsMessage(ctx, user);
return false;
}
}

if (min_completed_orders > 0) {
if (user.trades_completed < min_completed_orders) {
await messages.notMeetingRequirementsMessage(ctx, user);
return false;
}
}

return true;
};
8 changes: 8 additions & 0 deletions bot/modules/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export const configure = (bot: Telegraf<CommunityContext>) => {
ctx.reply(err.message);
}
});
bot.command('/requirements', userMiddleware, async ctx => {
try {
const { user } = ctx;
await ctx.scene.enter(Scenes.Requirements.id, { user });
} catch (err: any) {
ctx.reply(err.message);
}
});
};

export { Scenes };
3 changes: 2 additions & 1 deletion bot/modules/user/scenes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Settings from './settings';
import Requirements from './requirements';

export default { Settings };
export default { Settings, Requirements };
214 changes: 214 additions & 0 deletions bot/modules/user/scenes/requirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { Scenes } from 'telegraf';
import {
CommunityContext,
CommunityWizardState,
} from '../../community/communityContext';
import { Message } from 'telegraf/typings/core/types/typegram';
import { logger } from '../../../../logger';

function make() {
const resetMessage = async (ctx: CommunityContext, next: () => void) => {
const state = ctx.scene.state as CommunityWizardState;
delete state.feedback;
delete state.error;
next();
};
async function mainData(ctx: CommunityContext) {
const state = ctx.scene.state as CommunityWizardState;
const { user } = state;
return {
min_days_using_bot:
user.counterparty_requirements?.min_days_using_bot || 0,
min_completed_orders:
user.counterparty_requirements?.min_completed_orders || 0,
};
}
async function updateMessage(ctx: CommunityContext) {
try {
const state = ctx.scene.state as CommunityWizardState;
ctx.i18n.locale(state.language);
const { message, error } = state;

if (message === undefined) throw new Error('message is undefined');

const errorText = (error => {
if (!error) return;
return '<strong>⚠️ ERROR</strong>\n' + ctx.i18n.t(error.i18n, error);
Comment thread
Luquitasjeffrey marked this conversation as resolved.
})(error);
const feedbackText = (feedback => {
if (!feedback) return;
if (typeof feedback === 'string') return feedback;
return ctx.i18n.t(feedback.i18n, feedback);
})(state.feedback);
const extras = [errorText, feedbackText].filter(e => e);

const main = ctx.i18n.t('user_requirements', await mainData(ctx));
const str = [main, ...extras].filter(e => e).join('\n');

const messageChanged = str !== message.text;
if (!messageChanged) return;

const msg = await ctx.telegram.editMessageText(
message.chat.id,
message.message_id,
undefined,
str,
{
parse_mode: 'HTML',
link_preview_options: { is_disabled: true },
} as any,
);
state.message = msg as Message.TextMessage;
state.message.text = str;
} catch (err) {
logger.error(err);
}
}
async function initHandler(ctx: CommunityContext) {
try {
const state = ctx.scene.state as CommunityWizardState;
const { user } = state;
state.language = user.lang || ctx.from?.language_code;
const str = ctx.i18n.t('user_requirements', await mainData(ctx));
const msg = await ctx.reply(str, { parse_mode: 'HTML' });
state.message = msg;
state.message.text = str;
} catch (err) {
logger.error(err);
}
}
const scene = new Scenes.WizardScene(
'USER_COUNTERPARTY_REQUIREMENTS',
async (ctx: CommunityContext) => {
const state = ctx.scene.state as CommunityWizardState;
ctx.user = state.user;
if (!state.message) return initHandler(ctx);
await ctx.deleteMessage();
state.error = {
i18n: 'generic_error',
};
await updateMessage(ctx);
},
);

scene.command(
'counterpartyage',
resetMessage,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 user = state.user;
const maxAge = parseInt(
process.env.MAX_COUNTERPARTY_AGE_REQUIREMENT || '30',
);
if (min_days > maxAge) {
state.error = {
i18n: 'invalid_range',
command: '/counterpartyage',
max: maxAge,
};
return await updateMessage(ctx);
}
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) {
const state = ctx.scene.state as CommunityWizardState;
state.error = {
i18n: 'invalid_number',
};
await updateMessage(ctx);
}
Comment on lines +131 to +137
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid classifying all exceptions as invalid_number.

Right now, non-validation failures (e.g., DB save or Telegram API issues) are surfaced to users as input errors, which is misleading and makes incidents harder to diagnose.

Proposed fix
       } catch (err) {
         const state = ctx.scene.state as CommunityWizardState;
-        state.error = {
-          i18n: 'invalid_number',
-        };
+        if (err instanceof Error && err.message === 'NotValidNumber') {
+          state.error = { i18n: 'invalid_number' };
+        } else {
+          logger.error(err);
+          state.error = { i18n: 'generic_error' };
+        }
         await updateMessage(ctx);
       }
@@
       } catch (err) {
         const state = ctx.scene.state as CommunityWizardState;
-        state.error = {
-          i18n: 'invalid_number',
-        };
+        if (err instanceof Error && err.message === 'NotValidNumber') {
+          state.error = { i18n: 'invalid_number' };
+        } else {
+          logger.error(err);
+          state.error = { i18n: 'generic_error' };
+        }
         await updateMessage(ctx);
       }

Also applies to: 179-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bot/modules/user/scenes/requirements.ts` around lines 131 - 137, The catch
block that sets state.error = { i18n: 'invalid_number' } in the handler using
ctx.scene.state (CommunityWizardState) should only mark validation/parsing
errors as 'invalid_number'; change the catch to inspect the caught err (e.g.,
instanceof ValidationError, isAxiosError, err.name/message or a custom
validation check) and: for validation failures set state.error.i18n =
'invalid_number' and call updateMessage(ctx); for all other errors log the full
error (using your logger) and set a different state.error (e.g., i18n:
'internal_error' or a generic error key) or rethrow so DB/Telegram/API failures
aren’t presented as user input errors; apply the same fix to the other catch
block referenced around the second occurrence (the block at lines 179-185) and
ensure updateMessage(ctx) is called with the appropriate state in each branch.

},
);

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)
throw new Error('NotValidNumber');
const user = state.user;
const maxOrders = parseInt(
process.env.MAX_COUNTERPARTY_ORDERS_REQUIREMENT || '10',
);
if (min_orders > maxOrders) {
state.error = {
i18n: 'invalid_range',
command: '/counterpartyorders',
max: maxOrders,
};
return await updateMessage(ctx);
}
if (!user.counterparty_requirements) {
user.counterparty_requirements = {
min_days_using_bot: 0,
min_completed_orders: 0,
};
}
user.counterparty_requirements.min_completed_orders = min_orders;
await user.save();
state.feedback = {
i18n: 'counterpartyorders_updated',
orders: min_orders,
};
await updateMessage(ctx);
} catch (err) {
const state = ctx.scene.state as CommunityWizardState;
state.error = {
i18n: 'invalid_number',
};
await updateMessage(ctx);
}
},
);

scene.command('reset', resetMessage, async (ctx: CommunityContext) => {
try {
await ctx.deleteMessage();
const state = ctx.scene.state as CommunityWizardState;
const user = state.user;
user.counterparty_requirements = {
min_days_using_bot: 0,
min_completed_orders: 0,
};
await user.save();
state.feedback = {
i18n: 'requirements_reset',
};
await updateMessage(ctx);
} catch (err) {
(ctx.scene.state as CommunityWizardState).error = {
i18n: 'generic_error',
};
await updateMessage(ctx);
}
});

return scene;
}

export default make();
18 changes: 18 additions & 0 deletions locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ help: |
/setaddress <_lightning Adresse / off_> - Ermöglicht es dem Käufer, eine statische Zahlungsadresse (Lightning-Adresse) einzurichten, _off_ zum Deaktivieren
/setlang - Ermöglicht dem Benutzer, die Sprache zu ändern
/settings - Zeigt die aktuellen Einstellungen des Benutzers an
/requirements - Den Assistenten für Anforderungen an den Vertragspartner aufrufen
/listorders - Benutze diesen Befehl, um alle deine ausstehenden Transaktionen aufzulisten
/listcurrencies - Listet alle FIAT Währungen auf, die wir verwenden können
/fiatsent <_order id_> - Der Käufer teilt mit, dass er dem Verkäufer das FIAT-Geld geschickt hat
Expand Down Expand Up @@ -706,3 +707,20 @@ unblock_failed: "Fehler beim Freigeben des Benutzers"
check_solvers: Ihre Community ${communityName} hat keine Solver. Bitte fügen Sie innerhalb von ${remainingDays} Tagen mindestens einen hinzu, um zu verhindern, dass die Community deaktiviert wird.
check_solvers_last_warning: Ihre Community ${communityName} hat keine Solver. Bitte fügen Sie noch heute mindestens einen hinzu, um zu verhindern, dass die Community deaktiviert wird.
image_processing_error: Wir hatten einen Fehler beim Verarbeiten des Bildes, bitte warten Sie ein paar Minuten und versuchen Sie es erneut.

user_requirements: |
<strong>Anforderungen an den Vertragspartner</strong>
Erforderliches Alter des Vertragspartners: ${min_days_using_bot} Tage Bot-Nutzung.
Abgeschlossene Aufträge: ${min_completed_orders}

<strong># HILFE</strong>
/counterpartyage &lt;tage&gt; - Legt das Mindestalter (in Tagen) fest, das der Vertragspartner haben muss, um deine Aufträge anzunehmen.
/counterpartyorders &lt;aufträge&gt; - Legt die Mindestanzahl abgeschlossener Aufträge für den Vertragspartner fest, damit dieser deine Aufträge annehmen kann.
/reset - Setzt diese Konfiguration auf die Standardwerte zurück
/exit - Um diesen Assistenten zu verlassen
counterpartyage_updated: Altersanforderung des Vertragspartners auf ${days} Tage aktualisiert.
counterpartyorders_updated: Anforderung für abgeschlossene Aufträge des Vertragspartners auf ${orders} aktualisiert.
requirements_reset: Anforderungen an den Vertragspartner wurden auf Standardwerte zurückgesetzt.
not_meeting_requirements: Du erfüllst nicht die Anforderungen des Vertragspartners, um diesen Auftrag anzunehmen.
invalid_number: Ungültige Nummer.
invalid_range: Ungültiger Wert für den Parameter ${command}, bitte wählen Sie eine Zahl im Bereich [0 - ${max}]
Loading
Loading