Skip to content
Open
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: 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 @@ -975,6 +975,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 @@ -2224,4 +2238,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 } = generateRandomImage(user._id.toString());
Expand Down Expand Up @@ -127,6 +133,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 @@ -168,3 +177,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 @@ -707,3 +708,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