diff --git a/bot/modules/orders/commands.ts b/bot/modules/orders/commands.ts index 74ba87d9..7b55c8be 100644 --- a/bot/modules/orders/commands.ts +++ b/bot/modules/orders/commands.ts @@ -12,6 +12,7 @@ import * as ordersActions from '../../ordersActions'; import { deletedCommunityMessage } from './messages'; import { getCommunityInfo, + getCommunityByIdentifier, isCurrencySupported, } from '../../../util/communityHelper'; import { takebuy, takesell, takebuyValidation } from './takeOrder'; @@ -50,18 +51,15 @@ const sell = async (ctx: MainContext) => { const sellOrderParams = await validateSellOrder(ctx); if (!sellOrderParams) return; - const { amount, fiatAmount, fiatCode, paymentMethod } = sellOrderParams; + const { amount, fiatAmount, fiatCode, paymentMethod, communityName } = + sellOrderParams; let priceMargin = sellOrderParams.priceMargin; priceMargin = isFloat(priceMargin) ? parseFloat(priceMargin.toFixed(2)) : parseInt(priceMargin); - // Optimized community lookup - single database query instead of multiple - const communityInfo = await getCommunityInfo( - user, - ctx.message?.chat.type || 'private', - ctx.message?.chat as Chat.UserNameChat, - ); + const communityInfo = await resolveOrderCommunity(ctx, user, communityName); + if (!communityInfo) return; const { community, communityId, isBanned } = communityInfo; @@ -119,18 +117,15 @@ const buy = async (ctx: MainContext) => { const buyOrderParams = await validateBuyOrder(ctx); if (!buyOrderParams) return; - const { amount, fiatAmount, fiatCode, paymentMethod } = buyOrderParams; + const { amount, fiatAmount, fiatCode, paymentMethod, communityName } = + buyOrderParams; let priceMargin = buyOrderParams.priceMargin; priceMargin = isFloat(priceMargin) ? parseFloat(priceMargin.toFixed(2)) : parseInt(priceMargin); - // Optimized community lookup - single database query instead of multiple - const communityInfo = await getCommunityInfo( - user, - ctx.message?.chat.type || 'private', - ctx.message?.chat as Chat.UserNameChat, - ); + const communityInfo = await resolveOrderCommunity(ctx, user, communityName); + if (!communityInfo) return; const { community, communityId, isBanned } = communityInfo; @@ -182,6 +177,36 @@ const buy = async (ctx: MainContext) => { } }; +// Decides which community an order should be published to. +// When the user passes a community name in a private chat, the order is +// published to that community. Otherwise the default behaviour applies: the +// group's community when the command runs inside a group, or the user's +// default community in private. Returns null when the flow must stop because +// an error was already reported to the user. +async function resolveOrderCommunity( + ctx: MainContext, + user: UserDocument, + communityName?: string, +) { + // An explicit community always wins, in private chats and in groups alike. + // Only when none is passed do we fall back to the group's community (in a + // group) or the user's default community (in private). + if (communityName) { + const communityInfo = await getCommunityByIdentifier(user, communityName); + if (!communityInfo.community) { + await ctx.reply(ctx.i18n.t('community_not_found')); + return null; + } + return communityInfo; + } + + return getCommunityInfo( + user, + ctx.message?.chat.type || 'private', + ctx.message?.chat as Chat.UserNameChat, + ); +} + async function enterWizard( ctx: CommunityContext, user: UserDocument, diff --git a/bot/validations.ts b/bot/validations.ts index 045eb3f4..4636ae69 100644 --- a/bot/validations.ts +++ b/bot/validations.ts @@ -161,6 +161,23 @@ const processParameters = (args: string[]) => { return correctedArgs; }; +// The two optional trailing params of /buy and /sell are, in order, the price +// margin and the community. They are told apart by position, not by type: the +// first optional arg is always the price margin, the second is always the +// community. Positional parsing keeps numeric community identifiers (e.g. a +// Telegram group id) from being mistaken for a price margin, and makes any +// leftover non-numeric token fail the price-margin check instead of being +// silently swallowed as a community. +const parseOptionalOrderParams = (optionalArgs: string[]) => { + // priceMargin is kept loosely typed because downstream callers feed it + // through isFloat/parseInt/isNaN, matching the previous untyped behaviour. + const priceMargin: any = optionalArgs[0] === '' ? undefined : optionalArgs[0]; + const communityName: string | undefined = + optionalArgs[1] === '' ? undefined : optionalArgs[1]; + + return { priceMargin, communityName }; +}; + const validateSellOrder = async (ctx: MainContext) => { try { let args = ctx.state.command.args; @@ -170,7 +187,10 @@ const validateSellOrder = async (ctx: MainContext) => { } args = processParameters(args); - let [amount, fiatAmount, fiatCode, paymentMethod, priceMargin] = args; + let [amount, fiatAmount, fiatCode, paymentMethod] = args; + const { priceMargin, communityName } = parseOptionalOrderParams( + args.slice(4), + ); if (priceMargin && isNaN(priceMargin)) { await ctx.reply( @@ -244,6 +264,7 @@ const validateSellOrder = async (ctx: MainContext) => { fiatCode: fiatCode.toUpperCase(), paymentMethod, priceMargin, + communityName, }; } catch (error) { logger.error(error); @@ -260,7 +281,10 @@ const validateBuyOrder = async (ctx: MainContext) => { } args = processParameters(args); - let [amount, fiatAmount, fiatCode, paymentMethod, priceMargin] = args; + let [amount, fiatAmount, fiatCode, paymentMethod] = args; + const { priceMargin, communityName } = parseOptionalOrderParams( + args.slice(4), + ); if (priceMargin && isNaN(priceMargin)) { await ctx.reply( @@ -332,6 +356,7 @@ const validateBuyOrder = async (ctx: MainContext) => { fiatCode: fiatCode.toUpperCase(), paymentMethod, priceMargin, + communityName, }; } catch (error) { logger.error(error); diff --git a/locales/de.yaml b/locales/de.yaml index 65474515..1eff9c6e 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -71,7 +71,9 @@ must_be_numeric: ${fieldName} muss numerisch sein sats_amount: sats Wert fiat_amount: fiat Wert sell_correct_format: | - /sell \<_Anzahl sats_\> \<_fiat Wert_\> \<_fiat code_\> \<_Zahlungsmethode_\> \[_Zuschlag/Rabatt_\] + /sell \<_Anzahl sats_\> \<_fiat Wert_\> \<_fiat code_\> \<_Zahlungsmethode_\> \[_Zuschlag/Rabatt_\] \[_Community_\] + + Du kannst den Namen einer deiner Communities als letzten Parameter hinzufügen, um die Order dort statt in deiner Standard\-Community zu veröffentlichen\. Du musst den Zuschlag/Rabatt davor angeben \(nutze 0, wenn du keinen möchtest\)\. Um einen Verkaufsauftrag von 1000 Sats für 2 Euro \(EUR\) zu erstellen und anzugeben, dass die Zahlung in FIAT durch Überweisung oder Einzahlung am Geldautomaten erfolgt, musst du \<\> und \[\] weglassen\. @@ -85,7 +87,9 @@ sell_correct_format: | `/sell 0 100\-500 EUR "Zahlungsmethode" 3` buy_correct_format: | - /buy \<_Anzahl sats_\> \<_fiat Wert_\> \<_fiat code_\> \<_Zahlungsmethode_\> \[_Zuschlag/Rabatt_\] + /buy \<_Anzahl sats_\> \<_fiat Wert_\> \<_fiat code_\> \<_Zahlungsmethode_\> \[_Zuschlag/Rabatt_\] \[_Community_\] + + Du kannst den Namen einer deiner Communities als letzten Parameter hinzufügen, um die Order dort statt in deiner Standard\-Community zu veröffentlichen\. Du musst den Zuschlag/Rabatt davor angeben \(nutze 0, wenn du keinen möchtest\)\. Um einen Auftrag über 1000 Sats für 2 \(EUR\) zu erstellen und anzugeben, dass die Zahlung per Überweisung erfolgt, musst du sowohl \<\> als auch \[\] weglassen\. @@ -241,8 +245,8 @@ must_be_number_or_range: 'Fiat_amount muss eine Zahl oder ein numerischer Bereic invalid_lightning_address: Ungültige Lightning-Adresse unavailable_lightning_address: Nicht verfügbare Lightning-Adresse ${la} help: | - /sell <_Anzahl sats_> <_fiat Wert_> <_fiat code_> <_Zahlungsmethode_> \[_Zuschlag/Rabatt_] - Erstellt einen Bitcoin Verkaufsauftrag - /buy <_Anzahl sats_> <_fiat Wert_> <_fiat code_> <_Zahlungsmethode_> \[_Zuschlag/Rabatt_] - Erstellt eine Bitcoin Kaufauftrag + /sell <_Anzahl sats_> <_fiat Wert_> <_fiat code_> <_Zahlungsmethode_> \[_Zuschlag/Rabatt_] \[_Community_] - Erstellt einen Bitcoin Verkaufsauftrag + /buy <_Anzahl sats_> <_fiat Wert_> <_fiat code_> <_Zahlungsmethode_> \[_Zuschlag/Rabatt_] \[_Community_] - Erstellt eine Bitcoin Kaufauftrag /takeorder <_order id_> - Ermöglicht Ihnen, eine Bestellung aus dem Chat mit dem Bot anzunehmen, ohne zu dem Kanal zu gehen, in dem sie veröffentlicht wurde /info - Zeigt zusätzliche Informationen über das Bot an /showusername - Schaltet die Anzeige des Benutzernamens in jedem neu erstellten Auftrag aus. Der Standardwert ist *nein* (false) diff --git a/locales/en.yaml b/locales/en.yaml index fe6c5aec..f0e22937 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -73,7 +73,9 @@ must_be_numeric: ${fieldName} must be numeric sats_amount: sats amount fiat_amount: fiat amount sell_correct_format: | - /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_community_\] + + You can add the name of one of your communities as the last parameter to publish the order there instead of your default community\. You must include the premium/discount before it \(use 0 if you don't want any\)\. In order to create a sell order of 1000 satoshis for 2 US dollars \(USD\) and indicate that the fiat payment is through transfer or ATM deposit, you must avoid \<\> and \[\]\. @@ -87,7 +89,9 @@ sell_correct_format: | `/sell 0 100\-500 USD "payment method" 3` buy_correct_format: | - /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_community_\] + + You can add the name of one of your communities as the last parameter to publish the order there instead of your default community\. You must include the premium/discount before it \(use 0 if you don't want any\)\. In order to create an order for 1000 satoshis for 2 \(USD\) and indicate that fiat payment is through transfer, you must to omit both \<\> and \[\]\. @@ -245,8 +249,8 @@ invalid_lightning_address: Invalid lightning address unavailable_lightning_address: Unavailable lightning address ${la} ln_address_temporarily_disabled: ⚠️ Lightning Address payouts are temporarily disabled for maintenance. Please send a regular Lightning invoice (BOLT11) for the exact order amount to receive your sats. help: | - /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] - Creates a Sell order - /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] - Creates a Purchase Order + /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] [community] - Creates a Sell order + /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] [community] - Creates a Purchase Order /takeorder <_order id_> - Allows the user to take an order from the chat with the bot without going to the channel where it was published /info - Shows additional info about the bot /showusername - Toggles off the username display in each new order created. Default value is set to false diff --git a/locales/es.yaml b/locales/es.yaml index 62cd4029..69896877 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -71,7 +71,9 @@ must_be_numeric: ${fieldName} debe ser numérico sats_amount: monto en sats fiat_amount: monto en fiat sell_correct_format: | - /sell \<_monto en sats_\> \<_monto en fiat_\> \<_código fiat_\> \<_método de pago_\> \[_prima/descuento_\] + /sell \<_monto en sats_\> \<_monto en fiat_\> \<_código fiat_\> \<_método de pago_\> \[_prima/descuento_\] \[_comunidad_\] + + Puedes agregar el nombre de una de tus comunidades como último parámetro para publicar la orden ahí en lugar de en tu comunidad por defecto\. Debes incluir la prima/descuento antes \(usa 0 si no quieres ninguno\)\. Para crear una venta de 1000 satoshis por 2 bolívares \(VES\) e indicar que el método de pago fiat es pago móvil, debes omitir los \<\> y los \[\]\. @@ -85,7 +87,9 @@ sell_correct_format: | `/sell 0 100\-500 ves "pago móvil" 3` buy_correct_format: | - /buy \<_monto en sats_\> \<_monto en fiat_\> \<_código fiat_\> \<_método de pago_\> \[_prima/descuento_\] + /buy \<_monto en sats_\> \<_monto en fiat_\> \<_código fiat_\> \<_método de pago_\> \[_prima/descuento_\] \[_comunidad_\] + + Puedes agregar el nombre de una de tus comunidades como último parámetro para publicar la orden ahí en lugar de en tu comunidad por defecto\. Debes incluir la prima/descuento antes \(usa 0 si no quieres ninguno\)\. Para crear una compra de 1000 satoshis por 2 bolívares \(VES\) e indicar que el método de pago fiat es pago móvil, debes omitir los \<\> y los \[\]\. @@ -242,8 +246,8 @@ invalid_lightning_address: Dirección lightning no válida unavailable_lightning_address: Dirección lightning ${la} no disponible ln_address_temporarily_disabled: ⚠️ Los pagos a Lightning Address están temporalmente deshabilitados por mantenimiento. Por favor envía una factura Lightning normal (BOLT11) por el monto exacto de la orden para recibir tus sats. help: | - /sell <_monto en sats_> <_monto en fiat_> <_código fiat_> <_método de pago_> [prima/descuento] - Crea una orden de venta - /buy <_monto en sats_> <_monto en fiat_> <_código fiat_> <_método de pago_> [prima/descuento] - Crea una orden de compra + /sell <_monto en sats_> <_monto en fiat_> <_código fiat_> <_método de pago_> [prima/descuento] [comunidad] - Crea una orden de venta + /buy <_monto en sats_> <_monto en fiat_> <_código fiat_> <_método de pago_> [prima/descuento] [comunidad] - Crea una orden de compra /takeorder <_order id_> - Permite tomar una orden desde el chat con el bot sin ir al canal donde fue publicada /info - Muestra información sobre el bot /showusername - Permite mostrar u ocultar el username en cada nueva orden creada, el valor predeterminado es no (falso) diff --git a/locales/fa.yaml b/locales/fa.yaml index acf196be..09bf4eed 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -74,7 +74,9 @@ must_be_numeric: '${fieldName} باید عدد باشد.' sats_amount: تعداد ساتوشی fiat_amount: مقدار فیات sell_correct_format: | - /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_community_\] + + می‌توانید نام یکی از اجتماع‌های خود را به عنوان آخرین پارامتر اضافه کنید تا سفارش به جای اجتماع پیش‌فرض شما در آنجا منتشر شود\. باید پیش از آن، حباب/تخفیف را وارد کنید \(اگر نمی‌خواهید 0 بگذارید\)\. جهت ایجاد سفارش فروش 1000 ساتوشی در ازای 300 هزار تومان \(IRT\) و اعلام «کارت به کارت» به عنوان روش پرداخت، باید از به کار بردن \<\> و \[\]\ اجتناب کنید\. @@ -88,7 +90,9 @@ sell_correct_format: | `/sell 0 300000\-500000 IRT "payment method" 3` buy_correct_format: | - /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_community_\] + + می‌توانید نام یکی از اجتماع‌های خود را به عنوان آخرین پارامتر اضافه کنید تا سفارش به جای اجتماع پیش‌فرض شما در آنجا منتشر شود\. باید پیش از آن، حباب/تخفیف را وارد کنید \(اگر نمی‌خواهید 0 بگذارید\)\. جهت ایجاد سفارش خرید 1000 ساتوشی در ازای 300 هزار تومان \(IRT\) و اعلام «کارت به کارت» به عنوان روش پرداخت، باید از به کار بردن \<\> و \[\]\ اجتناب کنید\. @@ -254,10 +258,10 @@ must_be_number_or_range: | invalid_lightning_address: نشانی لایتنینگ نامعتبر است. unavailable_lightning_address: 'نشانی لایتنینگ ${la} در دسترس نیست.' help: | - /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] + /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] [community] سفارش فروش ایجاد می‌کند. - /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] + /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] [community] سفارش خرید ایجاد می‌کند. /takeorder <_order id_> diff --git a/locales/fr.yaml b/locales/fr.yaml index df82ee9c..b201262a 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -73,7 +73,9 @@ must_be_numeric: ${fieldName} doit être numérique sats_amount: montant en sats fiat_amount: montant en fiat sell_correct_format: | - /sell \<_montant en sats_\> \<_montant en fiat_\> \<_code monnaie fiat_\> \<_méthode de paiement_\> \[_plus-value/remise_\] + /sell \<_montant en sats_\> \<_montant en fiat_\> \<_code monnaie fiat_\> \<_méthode de paiement_\> \[_plus-value/remise_\] \[_communauté_\] + + Vous pouvez ajouter le nom d'une de vos communautés comme dernier paramètre pour y publier l'ordre au lieu de votre communauté par défaut\. Vous devez indiquer la plus\-value/remise avant \(utilisez 0 si vous n'en voulez pas\)\. Pour créer une offre de vente de 1000 satoshis pour 6000 Francs CFA \(XOF\) et indiquer que le paiement fiat doit se faire via Wave ou Mobile Money, il faut omettre les caractères \<\> and \[\]\. @@ -87,7 +89,9 @@ sell_correct_format: | `/sell 0 50000\-100000 XOF "Wave ou Mobile Money" 3` buy_correct_format: | - /buy \<_montant en sats_\> \<_montant en fiat_\> \<_code monnaie fiat_\> \<_méthode de paiement_\> \[_plus-value/remise_\] + /buy \<_montant en sats_\> \<_montant en fiat_\> \<_code monnaie fiat_\> \<_méthode de paiement_\> \[_plus-value/remise_\] \[_communauté_\] + + Vous pouvez ajouter le nom d'une de vos communautés comme dernier paramètre pour y publier l'ordre au lieu de votre communauté par défaut\. Vous devez indiquer la plus\-value/remise avant \(utilisez 0 si vous n'en voulez pas\)\. Pour créer une offre d'achat de 1000 satoshis pour 6000 Francs CFA \(XOF\) et indiquer que le paiement fiat doit se faire via transfert en ligne, il faut omettre les caractères \<\> and \[\]\. @@ -243,8 +247,8 @@ must_be_number_or_range: 'Le montant fiat doit être un nombre entier ou une pla invalid_lightning_address: Adresse Lightning invalide ! unavailable_lightning_address: Adresse Lightning non disponible ${la} help: | - /sell <_montant en sats_> <_montant en fiat_> <_code fiat_> <_méthode de paiement_> [plus-value/remise] - Pour créer une offre de vente - /buy <_montant en sats_> <_montant en fiat_> <_code fiat_> <_méthode de paiement_> [plus-value/remise] - Pour créer une offre d'achat + /sell <_montant en sats_> <_montant en fiat_> <_code fiat_> <_méthode de paiement_> [plus-value/remise] [communauté] - Pour créer une offre de vente + /buy <_montant en sats_> <_montant en fiat_> <_code fiat_> <_méthode de paiement_> [plus-value/remise] [communauté] - Pour créer une offre d'achat /takeorder <_order id_> - Vous permet de prendre une commande depuis le chat avec le bot sans accéder au canal où elle a été publiée /info - Affiche des informations additionnelles à propos du bot /showusername - Désactive l'affichage du nom d'utilisateur dans chaque offre nouvellement créée. La valeur par défaut est définie sur false diff --git a/locales/it.yaml b/locales/it.yaml index 85359643..f50496b9 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -71,7 +71,9 @@ must_be_numeric: ${fieldName} deve essere numerico sats_amount: importo in sats fiat_amount: importo in fiat sell_correct_format: | - /sell \<_importo in sats_\> \<_importo in fiat_\> \<_fiat code_\> \<_metodo di pagamento_\> \[_surplus/sconto_\] + /sell \<_importo in sats_\> \<_importo in fiat_\> \<_fiat code_\> \<_metodo di pagamento_\> \[_surplus/sconto_\] \[_community_\] + + Puoi aggiungere il nome di una delle tue community come ultimo parametro per pubblicare l'ordine lì invece che nella tua community predefinita\. Devi indicare il surplus/sconto prima \(usa 0 se non ne vuoi\)\. Per creare un ordine di vendita di 1000 satoshi per 2 euro \(EUR\) e indicare che il pagamento in fiat avviene tramite bonifico o deposito ATM, occorre evitare di \<\> e \[\]\. @@ -85,7 +87,9 @@ sell_correct_format: | `/sell 0 100\-500 EUR "metodo di pagamento" 3` buy_correct_format: | - /buy \<_importo in sats_\> \<_importo in fiat_\> \<_fiat code_\> \<_metodo di pagamento_\> \[_surplus/discount_\] + /buy \<_importo in sats_\> \<_importo in fiat_\> \<_fiat code_\> \<_metodo di pagamento_\> \[_surplus/discount_\] \[_community_\] + + Puoi aggiungere il nome di una delle tue community come ultimo parametro per pubblicare l'ordine lì invece che nella tua community predefinita\. Devi indicare il surplus/sconto prima \(usa 0 se non ne vuoi\)\. Per creare un ordine di 1000 satoshi per 2 euro \(EUR\) e indicare che il pagamento in fiat avviene tramite bonifico, è necessario omettere entrambe le diciture \<\> e \[\]\. @@ -241,8 +245,8 @@ must_be_number_or_range: 'Importo in fiat deve essere un numero o un range numer invalid_lightning_address: Invalid lightning adress unavailable_lightning_address: Lightning adress non disponibile ${la} help: | - /sell <_importo in satoshi_> <_importo in fiat_> <_fiat code_> <_metodo di pagamento_> [surplus/sconto] - Crea un ordine di vendita - /buy <_importo in satoshi_> <_importo in fiat_> <_Fiat code_> <_metodo di pagamento_> [premium/discount] - Crea un ordine di acquisto + /sell <_importo in satoshi_> <_importo in fiat_> <_fiat code_> <_metodo di pagamento_> [surplus/sconto] [community] - Crea un ordine di vendita + /buy <_importo in satoshi_> <_importo in fiat_> <_Fiat code_> <_metodo di pagamento_> [premium/discount] [community] - Crea un ordine di acquisto /takeorder <_order id_> - Permette di prendere un ordine dalla chat con il bot senza passare al canale dove è stato pubblicato /info - Mostra indo aggiuntive sul Bot /showusername - Disattiva la visualizzazione del nome utente in ogni nuovo ordine creato. Il valore predefinito è settato su no (false) diff --git a/locales/ko.yaml b/locales/ko.yaml index 24fe08a5..3a2a1891 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -72,7 +72,9 @@ must_be_numeric: ${fieldName}은 반드시 숫자형이어야만 합니다. sats_amount: 비트코인 액수 (sats) fiat_amount: 법정화폐 액수 sell_correct_format: | - /sell \<_비트코인 액수_\> \<_법정화폐 액수_\> \<_거래 통화_\> \<_결제 방법_\> \[_프리미엄/할인_\] + /sell \<_비트코인 액수_\> \<_법정화폐 액수_\> \<_거래 통화_\> \<_결제 방법_\> \[_프리미엄/할인_\] \[_커뮤니티_\] + + 기본 커뮤니티 대신 특정 커뮤니티에 주문을 게시하려면 마지막 매개변수로 커뮤니티 이름을 추가할 수 있습니다\. 그 앞에 프리미엄/할인을 반드시 포함해야 합니다 \(원하지 않으면 0을 사용하세요\)\. 1000 \(KRW\_)에 500 사토시를 판매하는 주문을 생성하고 법정화폐 결제 방법을 명시할 때, 위의 예시에서 \<\>와 \[\]는 빼는 것을 잊지 마세요. @@ -86,7 +88,9 @@ sell_correct_format: | `/sell 0 10000\-50000 KRW "결제 방법" 3` buy_correct_format: | - /buy \<_비트코인 액수_\> \<_법정화폐 액수_\> \<_거래 통화_\> \<_결제 방법_\> \[_프리미엄/할인_\] + /buy \<_비트코인 액수_\> \<_법정화폐 액수_\> \<_거래 통화_\> \<_결제 방법_\> \[_프리미엄/할인_\] \[_커뮤니티_\] + + 기본 커뮤니티 대신 특정 커뮤니티에 주문을 게시하려면 마지막 매개변수로 커뮤니티 이름을 추가할 수 있습니다\. 그 앞에 프리미엄/할인을 반드시 포함해야 합니다 \(원하지 않으면 0을 사용하세요\)\. 500 사토시를 1000 \(KRW\)에 구매하는 주문을 생성하고 법정화폐 결제 방법을 명시할 때, 위의 예시에서 \<\>와 \[\]는 빼는 것을 잊지 마세요. @@ -242,8 +246,8 @@ must_be_number_or_range: 'Fiat_amount는 반드시 숫자나 <최소>-<최대> invalid_lightning_address: 유효하지 않은 라이트닝 주소입니다. unavailable_lightning_address: 사용할 수 없는 라이트닝 주소입니다 ${la} help: | - /sell <_사토시 금액_> <_법정화폐 금액_> <_법정화폐 코드_> <_결제 방법_> [프리미엄/할인율] - 판매 주문을 생성합니다. - /buy <_사토시 금액_> <_법정화폐 금액_> <_법정화폐 코드_> <_결제 방법_> [프리미엄/할인율] - 구매 주문을 생성합니다. + /sell <_사토시 금액_> <_법정화폐 금액_> <_법정화폐 코드_> <_결제 방법_> [프리미엄/할인율] [커뮤니티] - 판매 주문을 생성합니다. + /buy <_사토시 금액_> <_법정화폐 금액_> <_법정화폐 코드_> <_결제 방법_> [프리미엄/할인율] [커뮤니티] - 구매 주문을 생성합니다. /takeorder <_주문 ID_> - 따로 주문이 등록된 마켓 채널에 가지 않고, 봇 채널에서 주문 ID로 주문을 수락할 수 있게 해 줍니다. /info - 봇에 대한 추가적인 정보를 보여줍니다. /showusername - 새로운 주문이 생성될 때마다 사용자명을 표시할지 여부를 설정합니다. 기본값은 false입니다. diff --git a/locales/pt.yaml b/locales/pt.yaml index 274ea54e..e15a87d6 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -72,7 +72,9 @@ must_be_numeric: ${fieldName} deve ser numérico sats_amount: quantidade de sats fiat_amount: quantidade fiduciária sell_correct_format: | - /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /sell \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_comunidade_\] + + Você pode adicionar o nome de uma de suas comunidades como último parâmetro para publicar a ordem lá em vez de na sua comunidade padrão\. Você deve incluir o premium/desconto antes \(use 0 se não quiser nenhum\)\. Para criar uma oferta de venda de 1000 satoshis por 2 dólares americanos \(USD\) e indicar que o pagamento fiduciário é por transferência ou depósito no caixa eletrônico, você deve evitar \<\> e \[\]\. @@ -86,7 +88,9 @@ sell_correct_format: | `/sell 0 100\-500 USD "forma de pagamento" 3` buy_correct_format: | - /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] + /buy \<_sats amount_\> \<_fiat amount_\> \<_fiat code_\> \<_payment method_\> \[_premium/discount_\] \[_comunidade_\] + + Você pode adicionar o nome de uma de suas comunidades como último parâmetro para publicar a ordem lá em vez de na sua comunidade padrão\. Você deve incluir o premium/desconto antes \(use 0 se não quiser nenhum\)\. Para criar uma oferta e pedir 1000 Satoshis por 2 \(USD\) e indicar que o pagamento fiduciário é através de transferência, você deve omitir os dois \<\> e \[\]\. @@ -242,8 +246,8 @@ must_be_number_or_range: 'Fiat_amount deve ser um número ou intervalo numérico invalid_lightning_address: Inválida lightning address unavailable_lightning_address: Indisponível lightning address ${la} help: | - /sell premium/desconto - Criar uma ordem de venda - /buy premium/desconto - Cria um pedido de compra + /sell premium/desconto comunidade - Criar uma ordem de venda + /buy premium/desconto comunidade - Cria um pedido de compra /takeorder <_order id_> - Permite ao usuário fazer um pedido dentro do chat com o bot, sem precisar ir até o canal onde foi publicado /info - Mostra informações sobre o bot /showusername - Permite mostrar ou ocultar o nome de usuário em cada novo pedido criado, o valor padrão é não (falso) diff --git a/locales/ru.yaml b/locales/ru.yaml index 49b61552..2b472f9a 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -69,7 +69,9 @@ must_be_numeric: ${fieldName} must be numeric sats_amount: сумма в сатоши fiat_amount: сумма в валюте sell_correct_format: | - /sell \<_сумма в сатоши_\> \<_сумма в валюте_\> \<_код валюты_\> \<_метод платежа_\> \[_премия/дисконт_\] + /sell \<_сумма в сатоши_\> \<_сумма в валюте_\> \<_код валюты_\> \<_метод платежа_\> \[_премия/дисконт_\] \[_сообщество_\] + + Вы можете добавить название одного из ваших сообществ последним параметром, чтобы опубликовать ордер там вместо вашего сообщества по умолчанию\. Перед ним необходимо указать премию/дисконт \(используйте 0, если он не нужен\)\. Чтобы создать заявку на продажу 1000 сатоши за 50 рублей \(RUB\) с оплатой по номеру телефона: @@ -83,7 +85,9 @@ sell_correct_format: | `/sell 0 100\-500 rub "на мобильный" 3` buy_correct_format: | - /buy \<_сумма в сатоши_\> \<_сумма в валюте_\> \<_код валюты_\> \<_метод платежа_\> \[_премия/дисконт_\] + /buy \<_сумма в сатоши_\> \<_сумма в валюте_\> \<_код валюты_\> \<_метод платежа_\> \[_премия/дисконт_\] \[_сообщество_\] + + Вы можете добавить название одного из ваших сообществ последним параметром, чтобы опубликовать ордер там вместо вашего сообщества по умолчанию\. Перед ним необходимо указать премию/дисконт \(используйте 0, если он не нужен\)\. Чтобы создать заявку на покупку 1000 сатоши за 50 рублей \(RUB\) с оплатой по номеру телефона: @@ -240,8 +244,8 @@ must_be_number_or_range: 'сумма в валюте должно быть чи invalid_lightning_address: неверный адрес LN unavailable_lightning_address: адрес LN ${la} недоступен help: | - /sell <_сумма в сатоши_> <_сумма в валюте_> <_код валюты_> <_метод платежа_> [_премия/дисконт_] - Создать заявку на продажу - /buy <_сумма в сатоши_> <_сумма в валюте_> <_код валюты_> <_метод платежа_> [_премия/дисконт_] - Создать заявку на покупку + /sell <_сумма в сатоши_> <_сумма в валюте_> <_код валюты_> <_метод платежа_> [_премия/дисконт_] [_сообщество_] - Создать заявку на продажу + /buy <_сумма в сатоши_> <_сумма в валюте_> <_код валюты_> <_метод платежа_> [_премия/дисконт_] [_сообщество_] - Создать заявку на покупку /takeorder <_order id_> - Позволяет пользователю принять заказ из чата с ботом, не заходя на канал, где он был опубликован /info - Показать информацию о Боте /showusername - Позволяет показывать или скрывать имя пользователя в каждой новой созданной заявке, значение по умолчанию — нет (false) diff --git a/locales/uk.yaml b/locales/uk.yaml index bda2aa09..288b945c 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -70,7 +70,9 @@ must_be_numeric: ${fieldName} повинен бути числовим sats_amount: сума у сатоші fiat_amount: сума у валюті sell_correct_format: | - /sell \<_сума у сатоші_\> \<_сума у валюті_\> \<_код валюти_\> \<_метод платежу\> \[_премія/дисконт_\] + /sell \<_сума у сатоші_\> \<_сума у валюті_\> \<_код валюти_\> \<_метод платежу\> \[_премія/дисконт_\] \[_спільнота_\] + + Ви можете додати назву однієї зі своїх спільнот останнім параметром, щоб опублікувати ордер там замість вашої спільноти за замовчуванням\. Перед ним потрібно вказати премію/дисконт \(використовуйте 0, якщо він не потрібен\)\. Щоб створити заявку на продаж 1000 сатоші за 50 гривень (UAH) з оплатою за номером телефону: @@ -84,7 +86,9 @@ sell_correct_format: | `/sell 0 100\-500 uah "на мобільний" 3` buy_correct_format: | - /buy \<_сума у сатоші_\> \<_сума у валюті_\> \<_код валюти_\> \<_метод платежу_\> \[_премія/дисконт_\] + /buy \<_сума у сатоші_\> \<_сума у валюті_\> \<_код валюти_\> \<_метод платежу_\> \[_премія/дисконт_\] \[_спільнота_\] + + Ви можете додати назву однієї зі своїх спільнот останнім параметром, щоб опублікувати ордер там замість вашої спільноти за замовчуванням\. Перед ним потрібно вказати премію/дисконт \(використовуйте 0, якщо він не потрібен\)\. Щоб створити заявку на купівлю 1000 сатоші за 50 гривень (UAH) з оплатою за номером телефону: @@ -240,8 +244,8 @@ must_be_number_or_range: 'Сума у валюті має бути число а invalid_lightning_address: Невірна адреса LN unavailable_lightning_address: Адреса LN ${la} недоступна help: | - /sell <_сума у сатоші_> <_сума у валюті_> <_код валюти_> <_метод платежу_> [_премія/дисконт_] - Створити заявку на продаж - /buy <_сума у сатоші_> <_сума у валюті_> <_код валюти_> <_метод платежу_> [_премія/дисконт_] - Створити заявку на купівлю + /sell <_сума у сатоші_> <_сума у валюті_> <_код валюти_> <_метод платежу_> [_премія/дисконт_] [_спільнота_] - Створити заявку на продаж + /buy <_сума у сатоші_> <_сума у валюті_> <_код валюти_> <_метод платежу_> [_премія/дисконт_] [_спільнота_] - Створити заявку на купівлю /takeorder <_order id_> - дозволяє користувачеві приймати замовлення з чату з ботом, не переходячи на канал, де воно було опубліковано /info - Показати інформацію про Бот /showusername - Дозволяє показувати або приховувати ім'я користувача в кожній новій заявці, значення за замовчуванням — ні (false) diff --git a/tests/bot/orders-commands.spec.ts b/tests/bot/orders-commands.spec.ts new file mode 100644 index 00000000..916b8c33 --- /dev/null +++ b/tests/bot/orders-commands.spec.ts @@ -0,0 +1,228 @@ +export {}; + +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { Order } = require('../../models'); +const validations = require('../../bot/validations'); +const ordersActions = require('../../bot/ordersActions'); +const messages = require('../../bot/messages'); +const communityHelper = require('../../util/communityHelper'); +const { buy, sell } = require('../../bot/modules/orders/commands'); + +/** + * Command-path integration tests for /buy and /sell. + * + * These exercise the whole handler through resolveOrderCommunity and assert the + * community_id actually handed to createOrder. That is the integration where the + * previous private-vs-group routing bug lived: the helper-only tests would pass + * even with that bug present, because they never run the command handler. + */ +describe('/buy and /sell community routing (command path)', () => { + const user: any = { _id: 'user-1', default_community_id: undefined }; + + let createOrder: any; + + const buildCtx = (chatType: string, params: any) => { + const ctx: any = { + user, + i18n: { t: (k: string) => k }, + message: { chat: { type: chatType, id: -1001234567890 } }, + reply: sinon.stub().resolves(), + deleteMessage: sinon.stub().resolves(), + }; + // validateBuyOrder / validateSellOrder return the parsed params + validations.validateBuyOrder.resolves(params); + validations.validateSellOrder.resolves(params); + return ctx; + }; + + beforeEach(() => { + // isMaxPending -> not maxed out + sinon.stub(Order, 'countDocuments').resolves(0); + process.env.MAX_PENDING_ORDERS = process.env.MAX_PENDING_ORDERS || '5'; + + // Sellers are always allowed to continue + sinon.stub(validations, 'validateSeller').resolves(true); + sinon.stub(validations, 'validateBuyOrder'); + sinon.stub(validations, 'validateSellOrder'); + + // Currency supported by default; overridden per-test when needed + sinon.stub(communityHelper, 'isCurrencySupported').returns(true); + sinon.stub(communityHelper, 'getCommunityByIdentifier'); + sinon.stub(communityHelper, 'getCommunityInfo'); + + // Capture the payload sent to createOrder and return a truthy order + createOrder = sinon + .stub(ordersActions, 'createOrder') + .resolves({ _id: 'order-1' }); + + // Silence publishing / error messages + sinon.stub(messages, 'publishBuyOrderMessage').resolves(); + sinon.stub(messages, 'publishSellOrderMessage').resolves(); + sinon.stub(messages, 'bannedUserErrorMessage').resolves(); + sinon.stub(messages, 'currencyNotSupportedMessage').resolves(); + }); + + afterEach(() => sinon.restore()); + + const baseParams = { + amount: 1000, + fiatAmount: [100], + fiatCode: 'USD', + paymentMethod: 'cash', + priceMargin: 0, + }; + + it('routes /buy to an explicit community by numeric group id in a group chat', async () => { + const community = { _id: 'community-A', public: true, banned_users: [] }; + communityHelper.getCommunityByIdentifier.resolves({ + community, + communityId: 'community-A', + isBanned: false, + }); + + const ctx = buildCtx('supergroup', { + ...baseParams, + communityName: '-1009999999999', + }); + await buy(ctx); + + expect(communityHelper.getCommunityByIdentifier.calledOnce).to.equal(true); + expect(communityHelper.getCommunityByIdentifier.firstCall.args[1]).to.equal( + '-1009999999999', + ); + expect(createOrder.calledOnce).to.equal(true); + expect(createOrder.firstCall.args[3].community_id).to.equal('community-A'); + expect(createOrder.firstCall.args[3].type).to.equal('buy'); + }); + + it('explicit community takes precedence over the group community for /sell', async () => { + const explicit = { + _id: 'community-explicit', + public: true, + banned_users: [], + }; + communityHelper.getCommunityByIdentifier.resolves({ + community: explicit, + communityId: 'community-explicit', + isBanned: false, + }); + + const ctx = buildCtx('supergroup', { + ...baseParams, + communityName: 'explicitName', + }); + await sell(ctx); + + // The explicit destination wins; the group fallback is never consulted + expect(communityHelper.getCommunityInfo.called).to.equal(false); + expect(createOrder.firstCall.args[3].community_id).to.equal( + 'community-explicit', + ); + }); + + it('falls back to the group community when no explicit community is passed', async () => { + const groupCommunity = { + _id: 'community-group', + public: true, + banned_users: [], + }; + communityHelper.getCommunityInfo.resolves({ + community: groupCommunity, + communityId: 'community-group', + isBanned: false, + }); + + const ctx = buildCtx('supergroup', { + ...baseParams, + communityName: undefined, + }); + await buy(ctx); + + expect(communityHelper.getCommunityByIdentifier.called).to.equal(false); + expect(communityHelper.getCommunityInfo.calledOnce).to.equal(true); + expect(createOrder.firstCall.args[3].community_id).to.equal( + 'community-group', + ); + }); + + it('publishes to the explicit community from a private chat', async () => { + const community = { + _id: 'community-priv', + public: false, + banned_users: [], + }; + communityHelper.getCommunityByIdentifier.resolves({ + community, + communityId: 'community-priv', + isBanned: false, + }); + + const ctx = buildCtx('private', { + ...baseParams, + communityName: '-1001234567890', + }); + await buy(ctx); + + expect(createOrder.firstCall.args[3].community_id).to.equal( + 'community-priv', + ); + }); + + it('rejects and does not create an order when the user is banned', async () => { + const community = { _id: 'community-A', public: true, banned_users: [] }; + communityHelper.getCommunityByIdentifier.resolves({ + community, + communityId: 'community-A', + isBanned: true, + }); + + const ctx = buildCtx('private', { + ...baseParams, + communityName: 'someName', + }); + await sell(ctx); + + expect(messages.bannedUserErrorMessage.calledOnce).to.equal(true); + expect(createOrder.called).to.equal(false); + }); + + it('rejects and does not create an order when the currency is unsupported', async () => { + const community = { + _id: 'community-A', + public: true, + banned_users: [], + currencies: ['EUR'], + }; + communityHelper.getCommunityByIdentifier.resolves({ + community, + communityId: 'community-A', + isBanned: false, + }); + communityHelper.isCurrencySupported.returns(false); + + const ctx = buildCtx('private', { + ...baseParams, + communityName: 'someName', + }); + await buy(ctx); + + expect(messages.currencyNotSupportedMessage.calledOnce).to.equal(true); + expect(createOrder.called).to.equal(false); + }); + + it('stops without creating an order when an explicit community is not found', async () => { + communityHelper.getCommunityByIdentifier.resolves({ + community: null, + communityId: undefined, + isBanned: false, + }); + + const ctx = buildCtx('private', { ...baseParams, communityName: 'ghost' }); + await buy(ctx); + + expect(ctx.reply.calledWith('community_not_found')).to.equal(true); + expect(createOrder.called).to.equal(false); + }); +}); diff --git a/tests/bot/validation.spec.ts b/tests/bot/validation.spec.ts index 63a5f3a6..80ad4907 100644 --- a/tests/bot/validation.spec.ts +++ b/tests/bot/validation.spec.ts @@ -156,11 +156,34 @@ describe('Validations', () => { expect(result.priceMargin).to.equal('5'); }); - it('should return false if price margin is not a number', async () => { - ctx.state.command.args = ['10000', '100', 'USD', 'zelle', 'test']; + it('should reject a non-numeric price margin (positional)', async () => { + ctx.state.command.args = ['10000', '100', 'USD', 'zelle', 'mycommunity']; const result = await validateSellOrder(ctx); expect(result).to.equal(false); - expect(replyStub.calledOnce).to.be.equal(true); + expect(replyStub.calledOnce).to.equal(true); + }); + + it('should keep numeric community identifiers as the community', async () => { + ctx.state.command.args = [ + '10000', + '100', + 'USD', + 'zelle', + '5', + '-1001234567890', + ]; + const result = await validateSellOrder(ctx); + if (result === false) throw new Error('object expected'); + expect(result.priceMargin).to.equal('5'); + expect(result.communityName).to.equal('-1001234567890'); + }); + + it('should parse both price margin and community name', async () => { + ctx.state.command.args = ['0', '100', 'USD', 'zelle', '5', 'mycommunity']; + const result = await validateSellOrder(ctx); + if (result === false) throw new Error('object expected'); + expect(result.priceMargin).to.equal('5'); + expect(result.communityName).to.equal('mycommunity'); }); it('should work with ranges', async () => { @@ -230,11 +253,34 @@ describe('Validations', () => { expect(result.priceMargin).to.equal('5'); }); - it('should return false if price margin is not a number', async () => { - ctx.state.command.args = ['10000', '100', 'USD', 'zelle', 'test']; + it('should reject a non-numeric price margin (positional)', async () => { + ctx.state.command.args = ['10000', '100', 'USD', 'zelle', 'mycommunity']; const result = await validateBuyOrder(ctx); expect(result).to.equal(false); - expect(replyStub.calledOnce).to.be.equal(true); + expect(replyStub.calledOnce).to.equal(true); + }); + + it('should keep numeric community identifiers as the community', async () => { + ctx.state.command.args = [ + '10000', + '100', + 'USD', + 'zelle', + '5', + '-1001234567890', + ]; + const result = await validateBuyOrder(ctx); + if (result === false) throw new Error('object expected'); + expect(result.priceMargin).to.equal('5'); + expect(result.communityName).to.equal('-1001234567890'); + }); + + it('should parse both price margin and community name', async () => { + ctx.state.command.args = ['0', '100', 'USD', 'zelle', '5', 'mycommunity']; + const result = await validateBuyOrder(ctx); + if (result === false) throw new Error('object expected'); + expect(result.priceMargin).to.equal('5'); + expect(result.communityName).to.equal('mycommunity'); }); it('should work with ranges', async () => { diff --git a/tests/util/communityHelper.spec.ts b/tests/util/communityHelper.spec.ts new file mode 100644 index 00000000..2bd07012 --- /dev/null +++ b/tests/util/communityHelper.spec.ts @@ -0,0 +1,67 @@ +export {}; + +const { expect } = require('chai'); +const sinon = require('sinon'); +const { Community } = require('../../models'); +const { getCommunityByIdentifier } = require('../../util/communityHelper'); + +/** + * Routing tests for getCommunityByIdentifier. They assert the resolved + * community_id (not just parsed arguments) and, most importantly, the query + * shape used for the display-name fallback: + * - the name is matched exactly (case-sensitive), so "Foo" and "foo" cannot be + * confused now that name has only a case-sensitive unique index; + * - the name fallback is restricted to public communities, so a private + * community can only be reached through its canonical group identifier. + */ +describe('getCommunityByIdentifier routing', () => { + const user: any = { _id: 'user-1' }; + + afterEach(() => sinon.restore()); + + it('resolves by canonical group id (case-insensitive) for private communities', async () => { + const community = { + _id: 'community-private', + public: false, + banned_users: [], + }; + const findOne = sinon.stub(Community, 'findOne'); + findOne.onFirstCall().resolves(community); + + const result = await getCommunityByIdentifier(user, '-1001234567890'); + + expect(String(result.communityId)).to.equal('community-private'); + const groupQuery = findOne.firstCall.args[0]; + expect(groupQuery.group).to.be.an.instanceOf(RegExp); + expect(groupQuery.group.flags).to.contain('i'); + }); + + it('matches the display name exactly and only among public communities', async () => { + const community = { _id: 'community-foo', public: true, banned_users: [] }; + const findOne = sinon.stub(Community, 'findOne'); + findOne.onFirstCall().resolves(null); // no group match + findOne.onSecondCall().resolves(community); // name match + + const result = await getCommunityByIdentifier(user, 'foo'); + + expect(String(result.communityId)).to.equal('community-foo'); + const nameQuery = findOne.secondCall.args[0]; + // case-sensitive exact string, not a case-insensitive regex + expect(nameQuery.name).to.equal('foo'); + expect(nameQuery.name).to.not.be.an.instanceOf(RegExp); + // never falls back into private communities by name + expect(nameQuery.public).to.equal(true); + }); + + it('does not resolve a private community by its display name', async () => { + const findOne = sinon.stub(Community, 'findOne'); + findOne.onFirstCall().resolves(null); // no group match + // name fallback filters on public: true, so a private one is not returned + findOne.onSecondCall().resolves(null); + + const result = await getCommunityByIdentifier(user, 'PrivateName'); + + expect(result.community).to.equal(null); + expect(result.communityId).to.equal(undefined); + }); +}); diff --git a/util/communityHelper.ts b/util/communityHelper.ts index 60926e79..3c9f7db7 100644 --- a/util/communityHelper.ts +++ b/util/communityHelper.ts @@ -72,6 +72,51 @@ export const getCommunityInfo = async ( } }; +/** + * Look up an enabled community by the identifier the user typed. It first tries + * the community group (the @handle or telegram group id, the same identifier + * used by /setcomm), matched ignoring uppercase/lowercase since the group is + * stored lowercase and unique. It then falls back to the display name, matched + * exactly (case-sensitive) and restricted to public communities: the name is + * only case-sensitively unique, so an exact match stays unambiguous, and + * private communities can only be reached through their canonical group id. + * Also reports whether the given user is banned from it. + */ +export const getCommunityByIdentifier = async ( + user: UserDocument, + identifier: string, +): Promise => { + try { + // Escape regex metacharacters so the identifier is matched literally + const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const groupRegex = new RegExp(`^${escaped}$`, 'i'); + + const community = + (await Community.findOne({ + group: groupRegex, + enabled: { $ne: false }, + })) || + (await Community.findOne({ + name: identifier, + public: true, + enabled: { $ne: false }, + })); + + if (!community) { + return { community: null, communityId: undefined, isBanned: false }; + } + + const isBanned = community.banned_users.some( + (buser: any) => String(buser.id) === String(user._id), + ); + + return { community, communityId: community._id, isBanned }; + } catch (error) { + logger.error(`Error in getCommunityByIdentifier: ${error}`); + return { community: null, communityId: undefined, isBanned: false }; + } +}; + /** * Check if a currency is supported by a community */