diff --git a/bot/middleware/stage.ts b/bot/middleware/stage.ts index 424eda3a..4f7f152c 100644 --- a/bot/middleware/stage.ts +++ b/bot/middleware/stage.ts @@ -2,7 +2,9 @@ import { Scenes } from 'telegraf'; import * as CommunityModule from '../modules/community'; import * as OrdersModule from '../modules/orders'; import * as UserModule from '../modules/user'; +import * as templatesScenes from '../modules/templates/scenes'; import { CommunityContext } from '../modules/community/communityContext'; + import { addInvoiceWizard, addFiatAmountWizard, @@ -27,7 +29,9 @@ export const stageMiddleware = () => { addInvoicePHIWizard, OrdersModule.Scenes.createOrder, UserModule.Scenes.Settings, + templatesScenes.templatesWizard, ]; + scenes.forEach(addGenericCommands); const stage = new Scenes.Stage(scenes, { ttl: 1200, // All wizards live 20 minutes diff --git a/bot/modules/templates/commands.ts b/bot/modules/templates/commands.ts new file mode 100644 index 00000000..2d25d4e0 --- /dev/null +++ b/bot/modules/templates/commands.ts @@ -0,0 +1,97 @@ +import { OrderTemplate, User } from '../../../models'; +import { IOrderTemplate } from '../../../models/order_template'; +import * as ordersActions from '../../ordersActions'; +import * as messages from './messages'; +import { + publishBuyOrderMessage, + publishSellOrderMessage, + tooManyPendingOrdersMessage, +} from '../../messages'; +import { MainContext, HasTelegram } from '../../start'; +import { isMaxPending } from '../orders/commands'; +import { logger } from '../../../logger'; +import { delay } from '../../../util'; + +export const renderTemplateList = async ( + ctx: MainContext, + userId: string, +): Promise => { + try { + const templates = await OrderTemplate.find({ creator_id: userId }); + const messageIds: number[] = []; + + if (templates.length === 0) { + const text = ctx.i18n.t('no_templates'); + const { keyboard } = messages.newTemplateButtonData(ctx.i18n); + const res = await ctx.reply(text, keyboard); + if (res) messageIds.push(res.message_id); + } else { + for (const template of templates) { + const { text, keyboard } = messages.singleTemplateData( + ctx.i18n, + template, + ); + const res = await ctx.reply(text, keyboard); + if (res) messageIds.push(res.message_id); + await delay(100); + } + const { text, keyboard } = messages.newTemplateButtonData(ctx.i18n); + const res = await ctx.reply(text, keyboard); + if (res) messageIds.push(res.message_id); + } + + return messageIds; + } catch (error) { + logger.error('Error in renderTemplateList:', error); + return []; + } +}; + +export const listTemplates = async (ctx: MainContext) => { + try { + await renderTemplateList(ctx, ctx.user._id); + } catch (error) { + logger.error(error); + } +}; + +export const publishFromTemplate = async ( + ctx: MainContext, + template: IOrderTemplate, +) => { + try { + const user = ctx.user || (await User.findOne({ tg_id: ctx.from?.id })); + if (!user) return; + + if (await isMaxPending(user)) { + return await tooManyPendingOrdersMessage(ctx, user, ctx.i18n); + } + + const order = await ordersActions.createOrder( + ctx.i18n, + ctx as any as HasTelegram, + user, + { + type: template.type, + amount: template.amount || 0, + fiatAmount: template.fiat_amount, + fiatCode: template.fiat_code, + paymentMethod: template.payment_method, + status: 'PENDING', + priceMargin: template.price_margin, + community_id: user.default_community_id, + }, + ); + + if (order) { + const publishFn = + template.type === 'buy' + ? publishBuyOrderMessage + : publishSellOrderMessage; + await publishFn(ctx as any, user, order, ctx.i18n, true); + } + } catch (error) { + logger.error(error); + await ctx.reply(ctx.i18n.t('generic_error')); + } +}; diff --git a/bot/modules/templates/index.ts b/bot/modules/templates/index.ts new file mode 100644 index 00000000..8718d607 --- /dev/null +++ b/bot/modules/templates/index.ts @@ -0,0 +1,15 @@ +import { Telegraf } from 'telegraf'; +import { CommunityContext } from '../community/communityContext'; +import { userMiddleware } from '../../middleware'; +import * as templatesScenes from './scenes'; + +export const configure = (bot: Telegraf) => { + bot.command('templates', userMiddleware, async ctx => { + await ctx.scene.enter(templatesScenes.TEMPLATES_WIZARD, { + user: ctx.user, + }); + }); + + // Note: Actions like 'create_template', 'publish_tpl_', etc. + // are now handled locally within the TEMPLATES_WIZARD scene. +}; diff --git a/bot/modules/templates/messages.ts b/bot/modules/templates/messages.ts new file mode 100644 index 00000000..64c592b3 --- /dev/null +++ b/bot/modules/templates/messages.ts @@ -0,0 +1,85 @@ +import { Markup } from 'telegraf'; +import { I18nContext } from '@grammyjs/i18n'; +import { IOrderTemplate } from '../../../models/order_template'; + +export const singleTemplateData = ( + i18n: I18nContext, + template: IOrderTemplate, +) => { + const action = template.type === 'buy' ? i18n.t('buying') : i18n.t('selling'); + const fiatAmount = + template.fiat_amount.length === 2 + ? `${template.fiat_amount[0]}-${template.fiat_amount[1]}` + : `${template.fiat_amount[0]}`; + + const amountStr = + template.amount > 0 + ? `${template.amount} ${i18n.t('sats')}` + : i18n.t('sats'); + + const isPremium = template.price_margin > 0; + const margin = + template.price_margin === 0 + ? '0' + : isPremium + ? `+${template.price_margin}` + : `${template.price_margin}`; + const rateStr = + template.amount > 0 ? '' : i18n.t('template_rate', { margin }); + + const text = i18n.t('template_card', { + action, + amountStr, + fiatAmount, + fiatCode: template.fiat_code, + paymentMethod: template.payment_method, + rateStr, + }); + + const keyboard = Markup.inlineKeyboard([ + Markup.button.callback( + i18n.t('template_publish_btn'), + `tpl_list_publish_${template._id}`, + ), + Markup.button.callback( + i18n.t('template_delete_btn'), + `tpl_list_delete_${template._id}`, + ), + ]); + + return { text, keyboard }; +}; + +export const newTemplateButtonData = (i18n: I18nContext) => { + return { + text: i18n.t('template_new_prompt'), + keyboard: Markup.inlineKeyboard([ + Markup.button.callback( + `➕ ${i18n.t('create_new_template')}`, + 'tpl_list_create', + ), + ]), + }; +}; + +export const templateSavedMessage = (i18n: I18nContext) => { + return i18n.t('template_saved'); +}; + +export const templateDeletedMessage = (i18n: I18nContext) => { + return i18n.t('template_deleted'); +}; + +export const confirmDeleteTemplateData = ( + i18n: I18nContext, + templateId: string, +) => { + const keyboard = Markup.inlineKeyboard([ + Markup.button.callback( + i18n.t('yes'), + `tpl_list_confirm_delete_${templateId}`, + ), + Markup.button.callback(i18n.t('no'), 'tpl_list_back'), + ]); + return { text: i18n.t('confirm_delete_template'), keyboard }; +}; diff --git a/bot/modules/templates/scenes.ts b/bot/modules/templates/scenes.ts new file mode 100644 index 00000000..7479ce75 --- /dev/null +++ b/bot/modules/templates/scenes.ts @@ -0,0 +1,615 @@ +import { Scenes, Markup } from 'telegraf'; +import { OrderTemplate } from '../../../models'; +import { getCurrency } from '../../../util'; +import { + CommunityContext, + CommunityWizardState, +} from '../community/communityContext'; +import { logger } from '../../../logger'; +import { createOrderWizardStatus } from '../orders/messages'; +import * as templatesMessages from './messages'; +import * as templatesCommands from './commands'; +import { Message } from 'telegraf/typings/core/types/typegram'; + +export const TEMPLATES_WIZARD = 'TEMPLATES_WIZARD'; + +interface TemplateWizardState extends Scenes.WizardSessionData { + user: any; + listMessageIds?: number[]; + statusMessage?: Message.TextMessage; + currentStatusText?: string; + type?: 'buy' | 'sell'; + currency?: string; + fiatAmount?: number[]; + amount?: number; + priceMargin?: number; + method?: string; + error?: string | null; + promptId?: number; + isUpdatingUI?: boolean; + updateUI?: () => Promise; +} + +const resetCreationState = (state: TemplateWizardState) => { + delete state.statusMessage; + delete state.currentStatusText; + delete state.type; + delete state.currency; + delete state.fiatAmount; + delete state.amount; + delete state.priceMargin; + delete state.method; + delete state.error; + delete state.promptId; + delete state.isUpdatingUI; + delete state.updateUI; +}; + +export const templatesWizard = new Scenes.WizardScene( + TEMPLATES_WIZARD, + // Step 0: List View & Management + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + + if (!state.user) { + logger.error('Templates wizard entered without user in state'); + return ctx.scene.leave(); + } + + resetCreationState(state); + + try { + if (state.listMessageIds) { + for (const msgId of state.listMessageIds) { + await ctx.telegram.deleteMessage(ctx.chat!.id, msgId).catch(() => {}); + } + } + + state.listMessageIds = await templatesCommands.renderTemplateList( + ctx as any, + state.user._id, + ); + + return ctx.wizard.next(); + } catch (err) { + logger.error('Error in templates list step:', err); + return ctx.scene.leave(); + } + }, + // Step 1: List Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + + if (ctx.callbackQuery) { + const data = (ctx.callbackQuery as any).data as string; + + if (data === 'tpl_list_create') { + await ctx.answerCbQuery().catch(() => {}); + // Cleanup list messages to avoid confusion + + if (state.listMessageIds) { + for (const msgId of state.listMessageIds) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, msgId) + .catch(() => {}); + } + state.listMessageIds = []; + } + ctx.wizard.cursor = 2; + return (ctx.wizard as any).steps[2](ctx); + } + + if (data.startsWith('tpl_list_publish_')) { + await ctx.answerCbQuery().catch(() => {}); + const id = data.replace('tpl_list_publish_', ''); + + // Clear all template messages to clean the UI + if (state.listMessageIds && state.listMessageIds.length > 0) { + for (const msgId of state.listMessageIds) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, msgId) + .catch(() => {}); + } + state.listMessageIds = []; + } + + const template = await OrderTemplate.findOne({ + _id: id, + creator_id: state.user._id, + }); + if (!template) { + await ctx.reply(ctx.i18n.t('template_not_found')); + return ctx.scene.leave(); + } + + await templatesCommands.publishFromTemplate(ctx as any, template); + + // EXIT WIZARD completely + return ctx.scene.leave(); + } + + if (data.startsWith('tpl_list_delete_')) { + await ctx.answerCbQuery().catch(() => {}); + const id = data.replace('tpl_list_delete_', ''); + const { text, keyboard } = templatesMessages.confirmDeleteTemplateData( + ctx.i18n, + id, + ); + if (ctx.callbackQuery.message) { + await ctx.telegram + .editMessageText( + ctx.chat!.id, + ctx.callbackQuery.message.message_id, + undefined, + text, + keyboard, + ) + .catch(() => {}); + } + return; + } + + if (data.startsWith('tpl_list_confirm_delete_')) { + await ctx.answerCbQuery().catch(() => {}); + const id = data.replace('tpl_list_confirm_delete_', ''); + await OrderTemplate.deleteOne({ _id: id, creator_id: state.user._id }); + await ctx.reply(templatesMessages.templateDeletedMessage(ctx.i18n)); + ctx.wizard.cursor = 0; + return (ctx.wizard as any).steps[0](ctx); + } + + if (data === 'tpl_list_back') { + await ctx.answerCbQuery().catch(() => {}); + ctx.wizard.cursor = 0; + return (ctx.wizard as any).steps[0](ctx); + } + } + + if (ctx.message) { + await ctx.deleteMessage().catch(() => {}); + } + }, + // Step 2: Setup Creation UI + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + + // Show first choice + const keyboard = Markup.inlineKeyboard([ + Markup.button.callback(ctx.i18n.t('buy'), 'tpl_type_buy'), + Markup.button.callback(ctx.i18n.t('sell'), 'tpl_type_sell'), + ]); + const prompt = await ctx.reply(ctx.i18n.t('enter_template_type'), keyboard); + state.promptId = prompt.message_id; + return ctx.wizard.next(); + }, + // Step 3: Type Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + if (ctx.callbackQuery) { + const data = (ctx.callbackQuery as any).data as string; + if (!data.startsWith('tpl_type_')) return; + await ctx.answerCbQuery().catch(() => {}); + state.type = data === 'tpl_type_buy' ? 'buy' : 'sell'; + + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + + if (!state.statusMessage) { + const { text } = createOrderWizardStatus( + ctx.i18n, + state as unknown as CommunityWizardState, + ); + const res = await ctx.reply(text); + state.currentStatusText = text; + state.statusMessage = res as Message.TextMessage; + + // Robust updateUI with lock to avoid race conditions + state.updateUI = async () => { + if (state.isUpdatingUI || !state.statusMessage) return; + const { text: newText } = createOrderWizardStatus( + ctx.i18n, + state as unknown as CommunityWizardState, + ); + if (state.currentStatusText === newText) return; + + state.isUpdatingUI = true; + try { + await ctx.telegram.editMessageText( + state.statusMessage.chat.id, + state.statusMessage.message_id, + undefined, + newText, + ); + state.currentStatusText = newText; + } catch (err: any) { + if (!err.description?.includes('message is not modified')) { + logger.warn('Failed to update template status UI:', err.message); + } + } finally { + state.isUpdatingUI = false; + } + }; + } + + await state.updateUI?.(); + + const buttons = ['USD', 'EUR', 'ARS', 'VES', 'COP', 'BRL'].map(c => + Markup.button.callback(c, `tpl_cur_${c}`), + ); + const rows = []; + for (let i = 0; i < buttons.length; i += 3) { + rows.push(buttons.slice(i, i + 3)); + } + const prompt = await ctx.reply( + ctx.i18n.t('choose_currency'), + Markup.inlineKeyboard(rows), + ); + state.promptId = prompt.message_id; + return ctx.wizard.next(); + } + if (ctx.message) await ctx.deleteMessage().catch(() => {}); + }, + // Step 4: Currency Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + let currencyCode: string | undefined; + + if (ctx.callbackQuery) { + const data = (ctx.callbackQuery as any).data as string; + if (!data.startsWith('tpl_cur_')) return; + await ctx.answerCbQuery().catch(() => {}); + currencyCode = data.replace('tpl_cur_', ''); + } else if (ctx.message && 'text' in ctx.message) { + currencyCode = ctx.message.text.toUpperCase(); + await ctx.deleteMessage().catch(() => {}); + } + + if (!currencyCode) return; + + const currency = getCurrency(currencyCode); + if (!currency) { + state.error = ctx.i18n.t('invalid_currency'); + await state.updateUI?.(); + return; + } + + state.currency = currency.code; + state.error = null; + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + await state.updateUI?.(); + + const prompt = await ctx.reply( + ctx.i18n.t('enter_currency_amount', { currency: state.currency }), + ); + state.promptId = prompt.message_id; + return ctx.wizard.next(); + }, + // Step 5: Amount Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + if (!ctx.message || !('text' in ctx.message)) return; + const text = ctx.message.text; + await ctx.deleteMessage().catch(() => {}); + + const tokens = text.split('-').map(s => s.trim()); + // Reject if any token is empty (e.g. "100-", "-100", "--") + if (tokens.some(t => t === '') || tokens.length > 2) { + state.error = ctx.i18n.t('must_be_number_or_range'); + await state.updateUI?.(); + return; + } + const inputs = tokens.map(Number); + // Reject non-finite values + if (inputs.some(n => !Number.isFinite(n))) { + state.error = ctx.i18n.t('not_number'); + await state.updateUI?.(); + return; + } + // Reject zeros + if (inputs.some(n => n === 0)) { + state.error = ctx.i18n.t('not_zero'); + await state.updateUI?.(); + return; + } + // For ranges enforce min < max + if (inputs.length === 2 && inputs[1] <= inputs[0]) { + state.error = ctx.i18n.t('must_be_number_or_range'); + await state.updateUI?.(); + return; + } + + state.fiatAmount = inputs; + state.error = null; + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + await state.updateUI?.(); + + if (inputs.length > 1) { + // Market price forced for range + state.amount = 0; + // Proceed to Margin directly + const margin = [ + '-5', + '-4', + '-3', + '-2', + '-1', + '+1', + '+2', + '+3', + '+4', + '+5', + ]; + const buttons = margin.map(m => + Markup.button.callback(m + '%', `tpl_margin_${m}`), + ); + const rows = []; + for (let i = 0; i < buttons.length; i += 5) { + rows.push(buttons.slice(i, i + 5)); + } + rows.push([ + Markup.button.callback( + ctx.i18n.t('no_premium_or_discount'), + 'tpl_margin_0', + ), + ]); + const prompt = await ctx.reply( + ctx.i18n.t('enter_premium_discount'), + Markup.inlineKeyboard(rows), + ); + state.promptId = prompt.message_id; + // Jump to margin handler (Index 7) + ctx.wizard.selectStep(7); + } else { + // Proceed to Sats Amount prompt + const button = Markup.button.callback( + ctx.i18n.t('market_price'), + 'marketPrice', + ); + const prompt = await ctx.reply( + ctx.i18n.t('enter_sats_amount'), + Markup.inlineKeyboard([button]), + ); + state.promptId = prompt.message_id; + return ctx.wizard.next(); // Index 6 + } + }, + // Step 6: Sats Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + + if (ctx.callbackQuery) { + const data = (ctx.callbackQuery as any).data as string; + if (data === 'marketPrice') { + await ctx.answerCbQuery().catch(() => {}); + state.amount = 0; + } else { + return; + } + } else if (ctx.message && 'text' in ctx.message) { + const input = Number(ctx.message.text); + await ctx.deleteMessage().catch(() => {}); + + if (!Number.isFinite(input)) { + state.error = ctx.i18n.t('not_number'); + await state.updateUI?.(); + return; + } + if (input < 0) { + state.error = ctx.i18n.t('not_negative'); + await state.updateUI?.(); + return; + } + state.amount = Math.floor(input); + } else { + return; + } + + state.error = null; + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + await state.updateUI?.(); + + if (state.amount > 0) { + // Fixed sats: bypass margin logic + state.priceMargin = 0; + const prompt = await ctx.reply(ctx.i18n.t('enter_payment_method')); + state.promptId = prompt.message_id; + ctx.wizard.selectStep(8); // Index 8: Method Handler + } else { + // Market price: need margin + const margin = [ + '-5', + '-4', + '-3', + '-2', + '-1', + '+1', + '+2', + '+3', + '+4', + '+5', + ]; + const buttons = margin.map(m => + Markup.button.callback(m + '%', `tpl_margin_${m}`), + ); + const rows = []; + for (let i = 0; i < buttons.length; i += 5) { + rows.push(buttons.slice(i, i + 5)); + } + rows.push([ + Markup.button.callback( + ctx.i18n.t('no_premium_or_discount'), + 'tpl_margin_0', + ), + ]); + const prompt = await ctx.reply( + ctx.i18n.t('enter_premium_discount'), + Markup.inlineKeyboard(rows), + ); + state.promptId = prompt.message_id; + return ctx.wizard.next(); // Index 7: Margin Handler + } + }, + + // Step 7: Margin Handler + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + let marginText: string | undefined; + + if (ctx.callbackQuery) { + const data = (ctx.callbackQuery as any).data as string; + if (!data.startsWith('tpl_margin_')) return; + await ctx.answerCbQuery().catch(() => {}); + marginText = data.replace('tpl_margin_', ''); + } else if (ctx.message && 'text' in ctx.message) { + marginText = ctx.message.text; + await ctx.deleteMessage().catch(() => {}); + } + + if (marginText === undefined) return; + + const marginVal = parseInt(marginText); + if (isNaN(marginVal)) { + state.error = ctx.i18n.t('not_number'); + await state.updateUI?.(); + return; + } + + state.priceMargin = marginVal; + state.error = null; + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + await state.updateUI?.(); + + const prompt = await ctx.reply(ctx.i18n.t('enter_payment_method')); + state.promptId = prompt.message_id; + return ctx.wizard.next(); + }, + // Step 8: Method Handler & Completion + async ctx => { + const state = ctx.wizard.state as unknown as TemplateWizardState; + if (!ctx.message || !('text' in ctx.message)) return; + const text = ctx.message.text; + await ctx.deleteMessage().catch(() => {}); + + state.method = text.trim(); + if (!state.method) { + state.error = ctx.i18n.t('enter_payment_method'); + await state.updateUI?.(); + return; + } + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + delete state.promptId; + } + await state.updateUI?.(); + + try { + const templateData = { + creator_id: state.user._id, + type: state.type, + fiat_code: state.currency, + fiat_amount: state.fiatAmount, + amount: state.amount || 0, + payment_method: state.method, + price_from_api: !state.amount, + price_margin: state.priceMargin, + }; + const template = new OrderTemplate(templateData); + await template.save(); + + if (state.statusMessage) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.statusMessage.message_id) + .catch(() => {}); + } + await ctx.reply(templatesMessages.templateSavedMessage(ctx.i18n)); + } catch (err) { + logger.error('Failed to save template:', err); + await ctx.reply(ctx.i18n.t('generic_error')); + } + + resetCreationState(state); + ctx.wizard.cursor = 0; + return (ctx.wizard as any).steps[0](ctx); + }, +); + +/** + * CRITICAL: Middleware registered to intercept commands. + * + * We block all commands EXCEPT: + * - /exit and /help (handled explicitly) + * - /templates (allowed to pass so it doesn't loop on entry) + * + * This ensures that if the user types /sell while looking at the templates list, + * they get the "wizard help" message and the message is deleted. + */ +templatesWizard.use(async (ctx, next) => { + if ( + ctx.message && + 'text' in ctx.message && + ctx.message.text.startsWith('/') + ) { + const text = ctx.message.text; + const state = ctx.wizard.state as unknown as TemplateWizardState; + + if (text === '/exit') { + if (state.statusMessage) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.statusMessage.message_id) + .catch(() => {}); + } + if (state.promptId) { + await ctx.telegram + .deleteMessage(ctx.chat!.id, state.promptId) + .catch(() => {}); + } + if (state.listMessageIds) { + for (const msgId of state.listMessageIds) { + await ctx.telegram.deleteMessage(ctx.chat!.id, msgId).catch(() => {}); + } + } + await ctx.scene.leave(); + return ctx.reply(ctx.i18n.t('wizard_exit')); + } + + if (text === '/help') { + return ctx.reply(ctx.i18n.t('wizard_help')); + } + + // Allow entry command to avoid infinite loop on scene enter + if (text === '/templates') { + return next(); + } + + await ctx.reply(ctx.i18n.t('wizard_help')); + await ctx.deleteMessage().catch(() => {}); + return; + } + return next(); +}); diff --git a/bot/start.ts b/bot/start.ts index 569fc7ab..84e6a7ab 100644 --- a/bot/start.ts +++ b/bot/start.ts @@ -35,6 +35,7 @@ import * as OrdersModule from './modules/orders'; import * as UserModule from './modules/user'; import * as DisputeModule from './modules/dispute'; import * as BlockModule from './modules/block'; +import * as TemplatesModule from './modules/templates'; import { rateUser, cancelAddInvoice, @@ -300,6 +301,7 @@ const initialize = ( CommunityModule.configure(bot); LanguageModule.configure(bot); BlockModule.configure(bot); + TemplatesModule.configure(bot); bot.command('release', userMiddleware, async ctx => { try { diff --git a/locales/de.yaml b/locales/de.yaml index 23f92e6d..2600547d 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -262,6 +262,8 @@ help: | /cancel <_order id_> - Einen Auftrag stornieren, die noch nicht angenommen wurde /cancelall - Storniere alle aufgegebenen und nicht angenommenen Aufträge /terms - Zeigt die Allgemeinen Geschäftsbedingungen bei der Nutzung des Bots an + Vorlagen: + /templates - Erstelle und verwalte deine Auftragsvorlagen Nostr: /setnpub <_nostr npub_> - Aktualisiert Ihren nostr Pubkey, dieser Befehl ist nur im /settings Wizard ausführbar @@ -704,3 +706,21 @@ 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. +# templates +no_templates: "Du hast noch keine Vorlagen. Erstelle eine!" +templates_list: "Deine Vorlagen:" +template_published: "✅ Auftrag aus Vorlage veröffentlicht!" +template_saved: "✅ Vorlage gespeichert." +template_deleted: "🗑 Vorlage gelöscht." +confirm_delete_template: "Diese Vorlage löschen?" +enter_template_type: "Auftragstyp wählen:" +template_summary: "Vorlagenübersicht:\nTyp: ${type}\nWährung: ${fiatCode}\nBetrag: ${fiatAmount}\nPreis: ${priceMode}\nMarge: ${margin}%\nZahlung: ${paymentMethod}" +template_not_found: "Vorlage nicht gefunden." +create_new_template: "Neue Vorlage erstellen" +template_publish_btn: "🚀 Veröffentlichen" +template_delete_btn: "🗑 Löschen" +template_card: "${action} ${amountStr} für ${fiatAmount} ${fiatCode}. Zahlung via ${paymentMethod}.${rateStr}" +template_rate: " Rate: ${margin}%." +template_new_prompt: "Neue Vorlage erstellen:" +buy: "Kaufe" +sell: "Verkaufe" diff --git a/locales/en.yaml b/locales/en.yaml index 117a99c1..bd48979b 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -265,6 +265,8 @@ help: | /cancel <_order id_> - Cancel an order that has not been taken yet /cancelall - Cancel all posted and untaken orders /terms - Shows the terms and conditions when using the bot + Templates: + /templates - Create and manage your order templates Nostr: /setnpub <_nostr npub_> - Updates your nostr pubkey, this command is only executable in the /settings wizard @@ -574,7 +576,7 @@ chat_not_found: Chat not found. Are you sure you added the bot to the group/chan not_member: You are not a member of that chat upgraded_to_supergroup: The chat was upgraded to 'supergroup' and the ID has changed, check the chat ID again community_deleted: This community was deleted due to inactivity. I have unlinked you from it, try to create the order again -dispute_too_soon: You can't start a dispute too soon, be patient and wait a few minutes for your counterparty to reply +dispute_too_soon: You can't start a dispute too soon, be patient and wait few minutes for your counterparty to reply maintenance: 🚨 Bot in maintenance, please try again later 🚨 # START modules/community @@ -709,3 +711,22 @@ unblock_failed: "Error unblocking the user" check_solvers: Your community ${communityName} does not have any solvers. Please add at least one within ${remainingDays} days to prevent the community from being disabled. check_solvers_last_warning: Your community ${communityName} does not have any solvers. Please add at least one today to prevent the community from being disabled. image_processing_error: We had an error processing the image, please wait a few minutes and try again + +# templates +no_templates: "You have no templates yet. Create one!" +templates_list: "Your templates:" +template_published: "✅ Order published from template!" +template_saved: "✅ Template saved." +template_deleted: "🗑 Template deleted." +confirm_delete_template: "Delete this template?" +enter_template_type: "Choose order type:" +template_summary: "Template summary:\nType: ${type}\nCurrency: ${fiatCode}\nAmount: ${fiatAmount}\nPrice: ${priceMode}\nMargin: ${margin}%\nPayment: ${paymentMethod}" +template_not_found: "Template not found." +create_new_template: "Create new template" +template_publish_btn: "🚀 Publish" +template_delete_btn: "🗑 Delete" +template_card: "${action} ${amountStr} for ${fiatAmount} ${fiatCode}. Payment via ${paymentMethod}.${rateStr}" +template_rate: " Rate: ${margin}%." +template_new_prompt: "Create a new template:" +buy: "Buy" +sell: "Sell" \ No newline at end of file diff --git a/locales/es.yaml b/locales/es.yaml index 6d75ce95..b08b34df 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -263,6 +263,9 @@ help: | /cancelall - Cancela todas las órdenes publicadas y que no han sido tomadas /terms - Muestra los términos y condiciones al usar el bot /privacy - Muestra la Política de Privacidad + Plantillas: + + /templates - Crea y gestiona tus plantillas de órdenes Nostr: /setnpub <_nostr npub_> - Actualiza tu nostr pubkey, este comando solo es ejecutable en el asistente de /settings @@ -706,3 +709,22 @@ unblock_failed: "Error al desbloquear al usuario" image_processing_error: Hemos tenido un error procesando la imagen, por favor espera unos minutos y vuelve a intentarlo. check_solvers: Tu comunidad ${communityName} no tiene ningún solucionador. Agregue al menos uno dentro de ${remainingDays} días para evitar que se deshabilite la comunidad. check_solvers_last_warning: Tu comunidad ${communityName} no tiene ningún solucionador. Agregue al menos uno hoy para evitar que la comunidad quede inhabilitada. + +# templates +no_templates: "No tienes plantillas aún. ¡Crea una!" +templates_list: "Tus plantillas:" +template_published: "✅ ¡Orden publicada desde la plantilla!" +template_saved: "✅ Plantilla guardada." +template_deleted: "🗑 Plantilla eliminada." +confirm_delete_template: "¿Eliminar esta plantilla?" +enter_template_type: "Elige el tipo de orden:" +template_summary: "Resumen de la plantilla:\nTipo: ${type}\nMoneda: ${fiatCode}\nMonto: ${fiatAmount}\nPrecio: ${priceMode}\nMargen: ${margin}%\nPago: ${paymentMethod}" +template_not_found: "Plantilla no encontrada." +create_new_template: "Crear nueva plantilla" +template_publish_btn: "🚀 Publicar" +template_delete_btn: "🗑 Eliminar" +template_card: "${action} ${amountStr} por ${fiatAmount} ${fiatCode}. Recibo pago por ${paymentMethod}.${rateStr}" +template_rate: " Tasa: ${margin}%." +template_new_prompt: "Crea una nueva plantilla:" +buy: "Comprar" +sell: "Vender" \ No newline at end of file diff --git a/locales/fa.yaml b/locales/fa.yaml index b1633743..1352bc02 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -254,69 +254,26 @@ must_be_number_or_range: | invalid_lightning_address: نشانی لایتنینگ نامعتبر است. unavailable_lightning_address: 'نشانی لایتنینگ ${la} در دسترس نیست.' help: | - /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] - سفارش فروش ایجاد می‌کند. - - /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] - سفارش خرید ایجاد می‌کند. - - /takeorder <_order id_> - به کاربر این امکان را می‌دهد که یک سفارش را در چت با ربات و بدون مراجعه به کانالی که سفارش در آن منتشر شده بود بپذیرد. - - /info - اطلاعات بیشتری درباره ربات را نمایش می‌دهد. - - /showusername - نمایش نام کاربری در هر سفارش جدید را فعال یا غیرفعال می‌کند. مقدار پیش‌فرض روی false (غیرفعال) تنظیم شده است. - - /showvolume - حجم معاملات در هنگام ایجاد هر سفارش را نمایش می‌دهد. مقدار پیش‌فرض false (عدم نمایش) است. - - /setinvoice - این دستور به خریدار امکان به‌روزرسانی صورتحساب لایتنینگ دریافت ساتوشی را می‌دهد. - - /setaddress <_lightning address / off_> - به خریدار امکان تعیین یک نشانی پرداخت ثابت (نشانی لایتنینگ) را می‌دهد. می‌توانید با مقدار _off_ آن را غیرفعال کنید. - - /setlang - امکان تغییر زبان را به کاربر می‌دهد. - - /settings - تنظیمات کنونی کاربر را نمایش می‌دهد. - - /listorders - از این دستور برای مشاهده لیستی از تمامی سفارش‌ها فعال خود استفاده کنید. - - /listcurrencies - لیستی از تمامی ارزهای قابل استفاده را بدون نرخ تبدیلشان به ساتوشی نمایش می‌دهد. - - /fiatsent <_order id_> - خریدار با این دستور اطلاع می‌دهد که پول فیات را به فروشنده ارسال کرده است. - - /release <_order id_> - فروشنده با این دستور ساتوشی‌ها را آزاد می‌کند. - - /dispute <_order id_> - مشاجره‌ای بین طرفین معامله را ثبت می‌‌کند. - - /block <_@username/id_> - مسدود کردن یک کاربر برای جلوگیری از تعامل او با سفارش‌های شما. - - /unblock <_@username/id_> - رفع مسدودیت یک کاربر. - - /blocklist - مشاهده لیست کاربران مسدود شده شما. - - /cancel <_order id_> - سفارشی که هنوز پذیرفته نشده را لغو می‌کند. - - /cancelall - تمامی سفارش‌های منتشر شده و پذیرفته نشده را لغو می‌کند. - - /terms - شرایط و ضوابط استفاده از ربات را نمایش می‌دهد. - + /sell <_sats amount_> <_fiat amount_> <_fiat code_> <_payment method_> [premium/discount] - یک سفارش فروش ایجاد می‌کند + /buy <_sats amount_> <_fiat amount_> <_Fiat code_> <_payment method_> [premium/discount] - یک سفارش خرید ایجاد می‌کند + /takeorder <_order id_> - به کاربر این امکان را می‌دهد تا بدون مراجعه به کانال انتشار سفارشات، از چت با ربات سفارش را بگیرد + /info - اطلاعات اضافی درباره ربات را نشان می دهد + /showusername - در هر سفارش جدید ایجاد شده، نمایش نام‌کاربری را خاموش می کند. مقدار پیش فرض روی false تنظیم شده است(نمایش داده نمیشود) + /showvolume - حجم معاملات را هنگام ایجاد هر سفارش نشان می‌دهد، مقدار پیش فرض نادرست است + /setinvoice - این دستور به خریدار اجازه می‌دهد تا فاکتور لایتنینگ را برای دریافت ساتوشی‌ها آپدیت کند + /setaddress <_lightning address / off_> - به خریدار امکان می‌دهد یک آدرس پرداخت ثابت (لایتنینگ آدرس) ایجاد کند، برای غیرفعال کردن عبارت_off_را جلو آن وارد کنید + /setlang - به کاربر امکان تغییر زبان را می‌دهد + /settings - تنظیمات فعلی کاربر را نمایش می‌دهد + /listorders - از این دستور برای فهرست کردن تمام تراکنش های معلق خود استفاده کنید + /listcurrencies - تمام ارزهایی را که می‌توانیم از آن‌ها استفاده کنیم، بدون نشان دادن مقدارشان به sat فهرست می‌کند + /fiatsent <_order id_> - خریدار اطلاع می‌دهد که قبلاً پول فیات را برای فروشنده ارسال کرده است + /release <_order id_> - فروشنده ساتوشی‌ها را آزاد می‌کند + /dispute <_order id_> - یک مشاجره بین طرفین شرکت کننده را آغاز می‌کند + /cancel <_order id_> - سفارشی را که هنوز پذیرفته نشده است لغو کنید + /cancelall - تمام سفارشات ارسال شده و انجام نشده را لغو کنید + /terms - شرایط و ضوابط استفاده از ربات نشان می‌دهد + قالب‌ها: + /templates - قالب‌های سفارش خود را ایجاد و مدیریت کنید Nostr: /setnpub <_nostr npub_> @@ -804,3 +761,22 @@ unblock_failed: "خطا در رفع مسدودیت کاربر" check_solvers: اجتماع شما ${communityName} هیچ داوری ندارد. لطفاً برای جلوگیری از غیرفعال شدن اجتماع، تا ${remainingDays} روز آینده حداقل یک داور به آن اضافه کنید. check_solvers_last_warning: اجتماع شما ${communityName} هیچ داوری ندارد. برای جلوگیری از غیرفعال شدن اجتماع، امروز حداقل یک داور به آن اضافه کنید. image_processing_error: هنگام پردازش تصویر با خطایی مواجه شدیم، لطفاً چند دقیقه صبر کرده و دوباره امتحان کنید. + +# templates +no_templates: "هنوز هیچ قالبی ندارید. یکی بسازید!" +templates_list: "قالب‌های شما:" +template_published: "✅ سفارش از قالب منتشر شد!" +template_saved: "✅ قالب ذخیره شد." +template_deleted: "🗑 قالب حذف شد." +confirm_delete_template: "این قالب حذف شود؟" +enter_template_type: "نوع سفارش را انتخاب کنید:" +template_summary: "خلاصه قالب:\nنوع: ${type}\nارز: ${fiatCode}\nمقدار: ${fiatAmount}\nقیمت: ${priceMode}\nحاشیه: ${margin}%\nپرداخت: ${paymentMethod}" +template_not_found: "قالب پیدا نشد." +create_new_template: "ساخت قالب جدید" +template_publish_btn: "🚀 انتشار" +template_delete_btn: "🗑 حذف" +template_card: "${action} ${amountStr} برای ${fiatAmount} ${fiatCode}. پرداخت از طریق ${paymentMethod}.${rateStr}" +template_rate: " حاشیه: ${margin}%." +template_new_prompt: "ساخت قالب جدید:" +buy: "خرید" +sell: "فروش" diff --git a/locales/fr.yaml b/locales/fr.yaml index 05e128a2..e94381c6 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -264,6 +264,8 @@ help: | /cancel <_order id_> - Annule une commande qui n'a pas encore été prise /cancelall - Annule toutes les commandes validées et non prises /terms - Affiche les termes et conditions lors de l'utilisation du bot + Modèles: + /templates - Créez et gérez vos modèles de commandes Nostr: /setnpub <_nostr npub_> - Met à jour ta clé publique nostr, cette commande n'est exécutable que dans l'assistant /settings @@ -703,3 +705,21 @@ unblock_failed: "Erreur lors du déblocage de l'utilisateur" check_solvers: Votre communauté ${communityName} ne possède aucun solveur. Veuillez en ajouter au moins un dans les ${remainingDays} jours pour éviter que la communauté ne soit désactivée. check_solvers_last_warning: Votre communauté ${communityName} ne possède aucun solveur. Veuillez en ajouter au moins un aujourd'hui pour éviter que la communauté ne soit désactivée. image_processing_error: Nous avons eu une erreur lors du traitement de l'image, veuillez attendre quelques minutes et réessayer. +# templates +no_templates: "Vous n'avez pas encore de modèles. Créez-en un !" +templates_list: "Vos modèles :" +template_published: "✅ Ordre publié depuis le modèle !" +template_saved: "✅ Modèle sauvegardé." +template_deleted: "🗑 Modèle supprimé." +confirm_delete_template: "Supprimer ce modèle ?" +enter_template_type: "Choisissez le type d'ordre :" +template_summary: "Résumé du modèle :\nType : ${type}\nDevise : ${fiatCode}\nMontant : ${fiatAmount}\nPrix : ${priceMode}\nMarge : ${margin}%\nPaiement : ${paymentMethod}" +template_not_found: "Modèle introuvable." +create_new_template: "Créer un nouveau modèle" +template_publish_btn: "🚀 Publier" +template_delete_btn: "🗑 Supprimer" +template_card: "${action} ${amountStr} pour ${fiatAmount} ${fiatCode}. Paiement via ${paymentMethod}.${rateStr}" +template_rate: " Taux : ${margin}%." +template_new_prompt: "Créer un nouveau modèle :" +buy: "Achat" +sell: "Vente" diff --git a/locales/it.yaml b/locales/it.yaml index 7ecb9e5a..bd79a0fd 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -262,6 +262,8 @@ help: | /cancel <_order id_> - Cancella un ordine che non è ancora stato accettato /cancelall - Cancella tutti gli ordini non ancora accettati /terms: mostra i termini e le condizioni quando si utilizza il bot + Modelli: + /templates - Crea e gestisci i tuoi modelli di ordine Nostr: /setnpub <_nostr npub_> - Aggiorna la tua nostr pubkey, questo comando è eseguibile solo nella procedura guidata /settings @@ -701,3 +703,21 @@ unblock_failed: "Errore nello sbloccare l'utente" check_solvers: La tua community ${communityName} non ha risolutori. Aggiungine almeno uno entro ${remainingDays} giorni per evitare che la community venga disabilitata. check_solvers_last_warning: La tua community ${communityName} non ha risolutori. Per favore aggiungine almeno uno oggi per evitare che la community venga disabilitata. image_processing_error: Abbiamo avuto un errore nel processare l'immagine, per favore attendi qualche minuto e prova di nuovo. +# templates +no_templates: "Non hai ancora modelli. Creane uno!" +templates_list: "I tuoi modelli:" +template_published: "✅ Ordine pubblicato dal modello!" +template_saved: "✅ Modello salvato." +template_deleted: "🗑 Modello eliminato." +confirm_delete_template: "Eliminare questo modello?" +enter_template_type: "Scegli il tipo di ordine:" +template_summary: "Riepilogo modello:\nTipo: ${type}\nValuta: ${fiatCode}\nImporto: ${fiatAmount}\nPrezzo: ${priceMode}\nMargine: ${margin}%\nPagamento: ${paymentMethod}" +template_not_found: "Modello non trovato." +create_new_template: "Crea nuovo modello" +template_publish_btn: "🚀 Pubblica" +template_delete_btn: "🗑 Elimina" +template_card: "${action} ${amountStr} per ${fiatAmount} ${fiatCode}. Pagamento tramite ${paymentMethod}.${rateStr}" +template_rate: " Tasso: ${margin}%." +template_new_prompt: "Crea un nuovo modello:" +buy: "Acquisto" +sell: "Vendita" diff --git a/locales/ko.yaml b/locales/ko.yaml index 76ef36fc..e7376ea1 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -263,6 +263,9 @@ help: | /terms - 사용자 이용 약관 및 면책 조항을 표시합니다. /cancel <_주문 id_> - 아직 수락되지 않은 주문을 취소합니다. /cancelall - 등록되었지만 아직 수락되지 않은 모든 주문들을 취소합니다. + 템플릿: + /templates - 주문 템플릿을 생성하고 관리하세요 + Nostr: /setnpub <_노스터 npub_> - 노스터 공개키를 설정합니다. 이 명령어는 /settings 명령어 수행 중에만 사용 가능합니다. @@ -701,3 +704,22 @@ unblock_failed: "사용자 차단 해제 중 오류 발생" check_solvers: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 ${remainingDays}일 이내에 하나 이상 추가하세요. check_solvers_last_warning: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 오늘 하나 이상 추가하세요. image_processing_error: 이미지 처리에 오류가 발생했습니다. 몇 분 후에 다시 시도해 주세요. + +# templates +no_templates: "아직 템플릿이 없습니다. 하나 생성하세요!" +templates_list: "당신의 템플릿:" +template_published: "✅ 템플릿에서 주문이 게시되었습니다!" +template_saved: "✅ 템플릿이 저장되었습니다." +template_deleted: "🗑 템플릿이 삭제되었습니다." +confirm_delete_template: "이 템플릿을 삭제할까요?" +enter_template_type: "주문 유형을 선택하세요:" +template_summary: "템플릿 요약:\n유형: ${type}\n통화: ${fiatCode}\n금액: ${fiatAmount}\n가격: ${priceMode}\n마진: ${margin}%\n결제: ${paymentMethod}" +template_not_found: "템플릿을 찾을 수 없습니다." +create_new_template: "새 템플릿 생성" +template_publish_btn: "🚀 게시" +template_delete_btn: "🗑 삭제" +template_card: "${action} ${amountStr} ${fiatAmount} ${fiatCode}에 대해 결제: ${paymentMethod}.${rateStr}" +template_rate: " 마진: ${margin}%." +template_new_prompt: "새 템플릿 생성:" +buy: "삽니다" +sell: "팝니다" \ No newline at end of file diff --git a/locales/pt.yaml b/locales/pt.yaml index 4ef9817a..cf985dac 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -263,6 +263,9 @@ help: | /cancel - Cancela um pedido que não foi feito /cancelall - Cancela todos os pedidos postados e não atendidos /terms - Mostra os termos e condições ao usar o bot + Modelos: + + /templates - Crie e gerencie seus modelos de pedidos Nostr: /setnpub <_nostr npub_> - Atualiza sua nostr pubkey, este comando só é executável no assistente /settings @@ -703,3 +706,21 @@ unblock_failed: "Erro ao desbloquear o usuário" check_solvers: Sua comunidade ${communityName} não possui solucionadores. Adicione pelo menos um dentro de ${remainingDays} dias para evitar que a comunidade seja desativada. check_solvers_last_warning: Sua comunidade ${communityName} não possui solucionadores. Adicione pelo menos um hoje para evitar que a comunidade seja desativada. image_processing_error: Tivemos um erro ao processar a imagem, por favor aguarde alguns minutos e tente novamente. +# templates +no_templates: "Você ainda não tem modelos. Crie um!" +templates_list: "Seus modelos:" +template_published: "✅ Ordem publicada a partir do modelo!" +template_saved: "✅ Modelo salvo." +template_deleted: "🗑 Modelo excluído." +confirm_delete_template: "Excluir este modelo?" +enter_template_type: "Escolha o tipo de ordem:" +template_summary: "Resumo do modelo:\nTipo: ${type}\nMoeda: ${fiatCode}\nQuantia: ${fiatAmount}\nPreço: ${priceMode}\nMargem: ${margin}%\nPagamento: ${paymentMethod}" +template_not_found: "Modelo não encontrado." +create_new_template: "Criar novo modelo" +template_publish_btn: "🚀 Publicar" +template_delete_btn: "🗑 Excluir" +template_card: "${action} ${amountStr} por ${fiatAmount} ${fiatCode}. Pagamento via ${paymentMethod}.${rateStr}" +template_rate: " Taxa: ${margin}%." +template_new_prompt: "Criar novo modelo:" +buy: "Comprando" +sell: "Vendendo" diff --git a/locales/ru.yaml b/locales/ru.yaml index 3d759562..7fa1bc1e 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -261,6 +261,10 @@ help: | /cancel <_order id_> - Отменить заявку до ее исполнения /cancelall - Отменить все выставленные заявки /terms - показать условия использования бота + Шаблоны: + /templates - Создавайте и управляйте своими шаблонами заказов + + Nostr: /setnpub <_nostr npub_> - Обновляет ваш публичный ключ nostr, эта команда выполняется только в мастере /settings @@ -704,3 +708,23 @@ unblock_failed: "Ошибка при разблокировке пользова check_solvers: В вашем сообществе ${communityName} нет решателей. Добавьте хотя бы одно в течение ${remainingDays} дн., чтобы сообщество не было отключено. check_solvers_last_warning: В вашем сообществе ${communityName} нет решателей. Пожалуйста, добавьте хотя бы один сегодня, чтобы предотвратить отключение сообщества. image_processing_error: У нас возникла ошибка при обработке изображения, пожалуйста, подождите несколько минут и попробуйте снова. + +# templates +no_templates: "У вас еще нет шаблонов. Создайте один!" +templates_list: "Ваши шаблоны:" +template_published: "✅ Публикация заказа из шаблона выполнена!" +template_saved: "✅ Шаблон сохранен." +template_deleted: "🗑 Шаблон удален." +confirm_delete_template: "Удалить этот шаблон?" +enter_template_type: "Выберите тип заказа:" +template_summary: "Краткое описание шаблона:\nТип: ${type}\nВалюта: ${fiatCode}\nСумма: ${fiatAmount}\nЦена: ${priceMode}\nМаржа: ${margin}%\nОплата: ${paymentMethod}" +template_not_found: "Шаблон не найден." +create_new_template: "Создать новый шаблон" +template_publish_btn: "🚀 Публиковать" +template_delete_btn: "🗑 Удалить" +template_card: "${action} ${amountStr} за ${fiatAmount} ${fiatCode}. Оплата через ${paymentMethod}.${rateStr}" +template_rate: " Маржа: ${margin}%." +template_new_prompt: "Создать новый шаблон:" +buy: "Покупаю" +sell: "Продаю" + diff --git a/locales/uk.yaml b/locales/uk.yaml index 80f42571..d06ac886 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -261,6 +261,8 @@ help: | /cancel <_order id_> - Скасувати заявку до її виконання /cancelall - Скасувати всі виставлені заявки /terms - Показати умови використання бота + Шаблони: + /templates - Створюйте та керуйте шаблонами замовлень Nostr: /setnpub <_nostr npub_> - Оновлює ваш nostr pubkey, цю команду можна виконати лише в майстрі /settings @@ -700,3 +702,23 @@ unblock_failed: "Помилка при розблокуванні користу check_solvers: У вашій спільноті ${communityName} немає розв’язувачів. Додайте принаймні одну протягом ${remainingDays} днів, щоб запобігти вимкненню спільноти. check_solvers_last_warning: У вашій спільноті ${communityName} немає розв’язувачів. Будь ласка, додайте принаймні одну сьогодні, щоб запобігти вимкненню спільноти. image_processing_error: У нас виникла помилка при обробці зображення, будь ласка, почекайте кілька хвилин і спробуйте знову. + +# templates +no_templates: "У вас ще немає шаблонів. Створіть один!" +templates_list: "Ваші шаблони:" +template_published: "✅ Замовлення опубліковано з шаблону!" +template_saved: "✅ Шаблон збережено." +template_deleted: "🗑 Шаблон видалено." +confirm_delete_template: "Видалити цей шаблон?" +enter_template_type: "Оберіть тип замовлення:" +template_summary: "Короткий опис шаблону:\nТип: ${type}\nВалюта: ${fiatCode}\nСума: ${fiatAmount}\nЦіна: ${priceMode}\nМаржа: ${margin}%\nОплата: ${paymentMethod}" +template_not_found: "Шаблон не знайдено." +create_new_template: "Створити новий шаблон" +template_publish_btn: "🚀 Опублікувати" +template_delete_btn: "🗑 Видалити" +template_card: "${action} ${amountStr} за ${fiatAmount} ${fiatCode}. Оплата через ${paymentMethod}.${rateStr}" +template_rate: " Маржа: ${margin}%." +template_new_prompt: "Створити новий шаблон:" +buy: "Купую" +sell: "Продаю" + diff --git a/models/index.ts b/models/index.ts index f68837bd..4dc16b5c 100644 --- a/models/index.ts +++ b/models/index.ts @@ -1,9 +1,19 @@ import User from './user'; import Order from './order'; +import OrderTemplate from './order_template'; import PendingPayment from './pending_payment'; import Community from './community'; import Dispute from './dispute'; import Config from './config'; import Block from './block'; -export { User, Order, PendingPayment, Community, Dispute, Config, Block }; +export { + User, + Order, + OrderTemplate, + PendingPayment, + Community, + Dispute, + Config, + Block, +}; diff --git a/models/order_template.ts b/models/order_template.ts new file mode 100644 index 00000000..196e8405 --- /dev/null +++ b/models/order_template.ts @@ -0,0 +1,38 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +export interface IOrderTemplate extends Document { + creator_id: string; + type: 'buy' | 'sell'; + fiat_code: string; + fiat_amount: number[]; + amount: number; + payment_method: string; + price_from_api: boolean; + price_margin: number; + created_at: Date; + updated_at: Date; +} + +const orderTemplateSchema = new Schema({ + creator_id: { type: String, required: true, index: true }, + type: { type: String, enum: ['buy', 'sell'], required: true }, + fiat_code: { type: String, required: true }, + fiat_amount: { type: [Number], required: true }, + amount: { type: Number, default: 0 }, + payment_method: { type: String, required: true }, + price_from_api: { type: Boolean, default: true }, + price_margin: { type: Number, default: 0 }, + created_at: { type: Date, default: Date.now }, + updated_at: { type: Date, default: Date.now }, +}); + +// Update updated_at on save +orderTemplateSchema.pre('save', function (next) { + this.updated_at = new Date(); + next(); +}); + +export default mongoose.model( + 'OrderTemplate', + orderTemplateSchema, +);