diff --git a/bot/messages.ts b/bot/messages.ts index 17354be0..21cbd687 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -730,8 +730,20 @@ const publishBuyOrderMessage = async ( let publishMessage = `⚡️🍊⚡️\n${order.description}\n`; publishMessage += `:${order._id}:`; - const channel = await getOrderChannel(order); - if (channel === undefined) throw new Error('channel is undefined'); + const channel = await getOrderChannel(order, bot.telegram); + if (channel === undefined) { + try { + await bot.telegram.sendMessage( + user.tg_id, + i18n.t('order_channel_validation_failed'), + ); + } catch (error) { + logger.error(error); + } + order.status = 'CLOSED'; + await order.save(); + return; + } // Get the community language if available let communityI18n = i18n; @@ -782,8 +794,20 @@ const publishSellOrderMessage = async ( try { let publishMessage = `⚡️🍊⚡️\n${order.description}\n`; publishMessage += `:${order._id}:`; - const channel = await getOrderChannel(order); - if (channel === undefined) throw new Error('channel is undefined'); + const channel = await getOrderChannel(order, ctx.telegram); + if (channel === undefined) { + try { + await ctx.telegram.sendMessage( + user.tg_id, + i18n.t('order_channel_validation_failed'), + ); + } catch (error) { + logger.error(error); + } + order.status = 'CLOSED'; + await order.save(); + return; + } // Get the community language if available let communityI18n = i18n; diff --git a/locales/de.yaml b/locales/de.yaml index 07e691ab..36db84c4 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -707,6 +707,7 @@ 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. +order_channel_validation_failed: "Ihre Bestellung konnte nicht veröffentlicht werden. Versuchen Sie, die Standard-Community mit /setcomm off zu deaktivieren, oder bitten Sie Ihren Community-Administrator, die Kanäle und Gruppen der Community neu zu konfigurieren." community_payment_methods: "Zahlungsmethoden" enter_community_payment_methods: "Gib die in deiner Community akzeptierten Zahlungsmethoden ein, getrennt durch Kommas (z.B.: Banküberweisung, Bargeld, PayPal)" current_payment_methods: "Aktuelle Zahlungsmethoden: ${methods}" diff --git a/locales/en.yaml b/locales/en.yaml index def6f4cd..7381528f 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -712,6 +712,7 @@ 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 +order_channel_validation_failed: "Unable to publish your order. Try disabling the default community /setcomm off, or ask your community administrator to reconfigure the community channels and groups." community_payment_methods: "Payment methods" enter_community_payment_methods: "Enter the payment methods accepted in your community, separated by commas (e.g.: Bank transfer, Cash, PayPal)" current_payment_methods: "Current payment methods: ${methods}" diff --git a/locales/es.yaml b/locales/es.yaml index 3825ad63..32a6aa6a 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -709,6 +709,7 @@ 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. +order_channel_validation_failed: "No se pudo publicar la orden, prueba desactivando la comunidad por defecto /setcomm off, o pídele al administrador de tu comunidad que reconfigure los canales y grupos de comunidad." community_payment_methods: "Métodos de pago" enter_community_payment_methods: "Ingresa los métodos de pago aceptados en tu comunidad, separados por comas (ej.: Transferencia bancaria, Efectivo, PayPal)" current_payment_methods: "Métodos de pago actuales: ${methods}" diff --git a/locales/fa.yaml b/locales/fa.yaml index 463206de..0f33386f 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -806,6 +806,7 @@ unblock_failed: "خطا در رفع مسدودیت کاربر" check_solvers: اجتماع شما ${communityName} هیچ داوری ندارد. لطفاً برای جلوگیری از غیرفعال شدن اجتماع، تا ${remainingDays} روز آینده حداقل یک داور به آن اضافه کنید. check_solvers_last_warning: اجتماع شما ${communityName} هیچ داوری ندارد. برای جلوگیری از غیرفعال شدن اجتماع، امروز حداقل یک داور به آن اضافه کنید. image_processing_error: هنگام پردازش تصویر با خطایی مواجه شدیم، لطفاً چند دقیقه صبر کرده و دوباره امتحان کنید. +order_channel_validation_failed: "امکان انتشار سفارش شما وجود ندارد. سعی کنید انجمن پیش‌فرض را با /setcomm off غیرفعال کنید یا از مدیر انجمن خود بخواهید کانال‌ها و گروه‌های انجمن را مجدداً پیکربندی کند." community_payment_methods: "روش‌های پرداخت" enter_community_payment_methods: "روش‌های پرداخت پذیرفته‌شده در اجتماع خود را با کاما از هم جدا کرده وارد کنید (مثال: انتقال بانکی، نقد، PayPal)" current_payment_methods: "روش‌های پرداخت فعلی: ${methods}" diff --git a/locales/fr.yaml b/locales/fr.yaml index e96d3e0b..6138d593 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -706,6 +706,7 @@ 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. +order_channel_validation_failed: "Impossible de publier votre commande. Essayez de désactiver la communauté par défaut avec /setcomm off, ou demandez à l'administrateur de votre communauté de reconfigurer les canaux et groupes de la communauté." community_payment_methods: "Méthodes de paiement" enter_community_payment_methods: "Entrez les méthodes de paiement acceptées dans votre communauté, séparées par des virgules (ex. : Virement bancaire, Espèces, PayPal)" current_payment_methods: "Méthodes de paiement actuelles : ${methods}" diff --git a/locales/it.yaml b/locales/it.yaml index 54aae511..da3d051c 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -704,6 +704,7 @@ 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. +order_channel_validation_failed: "Impossibile pubblicare il tuo ordine. Prova a disattivare la community predefinita con /setcomm off, o chiedi all'amministratore della tua community di riconfigurare i canali e i gruppi della community." community_payment_methods: "Metodi di pagamento" enter_community_payment_methods: "Inserisci i metodi di pagamento accettati nella tua community, separati da virgole (es.: Bonifico bancario, Contanti, PayPal)" current_payment_methods: "Metodi di pagamento attuali: ${methods}" diff --git a/locales/ko.yaml b/locales/ko.yaml index 8880aa1d..00a5e585 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -704,6 +704,7 @@ unblock_failed: "사용자 차단 해제 중 오류 발생" check_solvers: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 ${remainingDays}일 이내에 하나 이상 추가하세요. check_solvers_last_warning: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 오늘 하나 이상 추가하세요. image_processing_error: 이미지 처리에 오류가 발생했습니다. 몇 분 후에 다시 시도해 주세요. +order_channel_validation_failed: "주문을 등록할 수 없습니다. /setcomm off 로 기본 커뮤니티를 비활성화하거나, 커뮤니티 관리자에게 커뮤니티 채널 및 그룹을 재설정해 달라고 요청하세요." community_payment_methods: "결제 방법" enter_community_payment_methods: "커뮤니티에서 허용되는 결제 방법을 쉼표로 구분하여 입력하세요 (예: 은행 이체, 현금, PayPal)" current_payment_methods: "현재 결제 방법: ${methods}" diff --git a/locales/pt.yaml b/locales/pt.yaml index 7cfdebc2..9b7e2544 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -706,6 +706,7 @@ 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. +order_channel_validation_failed: "Não foi possível publicar a sua ordem. Tente desativar a comunidade padrão com /setcomm off ou peça ao administrador da comunidade para reconfigurar os canais e grupos da comunidade." community_payment_methods: "Métodos de pagamento" enter_community_payment_methods: "Insira os métodos de pagamento aceitos em sua comunidade, separados por vírgulas (ex.: Transferência bancária, Dinheiro, PayPal)" current_payment_methods: "Métodos de pagamento atuais: ${methods}" diff --git a/locales/ru.yaml b/locales/ru.yaml index 09956b27..03474603 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -707,6 +707,7 @@ unblock_failed: "Ошибка при разблокировке пользова check_solvers: В вашем сообществе ${communityName} нет решателей. Добавьте хотя бы одно в течение ${remainingDays} дн., чтобы сообщество не было отключено. check_solvers_last_warning: В вашем сообществе ${communityName} нет решателей. Пожалуйста, добавьте хотя бы один сегодня, чтобы предотвратить отключение сообщества. image_processing_error: У нас возникла ошибка при обработке изображения, пожалуйста, подождите несколько минут и попробуйте снова. +order_channel_validation_failed: "Не удалось опубликовать ваш ордер. Попробуйте отключить сообщество по умолчанию с помощью /setcomm off или попросите администратора сообщества перенастроить каналы и группы сообщества." community_payment_methods: "Способы оплаты" enter_community_payment_methods: "Введите способы оплаты, принятые в вашем сообществе, разделённые запятыми (напр.: Банковский перевод, Наличные, PayPal)" current_payment_methods: "Текущие способы оплаты: ${methods}" diff --git a/locales/uk.yaml b/locales/uk.yaml index 1176d9e4..d0d84596 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -703,6 +703,7 @@ unblock_failed: "Помилка при розблокуванні користу check_solvers: У вашій спільноті ${communityName} немає розв’язувачів. Додайте принаймні одну протягом ${remainingDays} днів, щоб запобігти вимкненню спільноти. check_solvers_last_warning: У вашій спільноті ${communityName} немає розв’язувачів. Будь ласка, додайте принаймні одну сьогодні, щоб запобігти вимкненню спільноти. image_processing_error: У нас виникла помилка при обробці зображення, будь ласка, почекайте кілька хвилин і спробуйте знову. +order_channel_validation_failed: "Не вдалося опублікувати ваше замовлення. Спробуйте вимкнути спільноту за замовчуванням за допомогою /setcomm off або попросіть адміністратора спільноти переналаштувати канали та групи спільноти." community_payment_methods: "Способи оплати" enter_community_payment_methods: "Введіть способи оплати, прийняті у вашій спільноті, розділені комами (напр.: Банківський переказ, Готівка, PayPal)" current_payment_methods: "Поточні способи оплати: ${methods}" diff --git a/models/community.ts b/models/community.ts index 0434174e..76b75fd8 100644 --- a/models/community.ts +++ b/models/community.ts @@ -4,7 +4,7 @@ import { isValidLanguage, SUPPORTED_LANGUAGES } from '../util/languages'; const CURRENCIES: number = parseInt(process.env.COMMUNITY_CURRENCIES || '10'); const arrayLimits = (val: any[]): boolean => { - return val.length > 0 && val.length <= 2; + return val.length <= 2; }; const currencyLimits = (val: string): boolean => { @@ -17,7 +17,7 @@ export interface IOrderChannel extends Document { } const OrderChannelSchema = new Schema({ - name: { type: String, required: true, trim: true }, + name: { type: String, unique: true, trim: true }, type: { type: String, enum: ['buy', 'sell', 'mixed'], @@ -65,7 +65,7 @@ const CommunitySchema = new Schema({ required: true, }, creator_id: { type: String }, - group: { type: String, trim: true }, // group Id or public name + group: { type: String, unique: true, sparse: true }, // group Id or public name order_channels: { // array of Id or public name of channels type: [OrderChannelSchema], @@ -74,7 +74,7 @@ const CommunitySchema = new Schema({ fee: { type: Number, min: 0, max: 100, default: 0 }, earnings: { type: Number, default: 0 }, // Sats amount to be paid to the community orders_to_redeem: { type: Number, default: 0 }, // Number of orders calculated to be redeemed - dispute_channel: { type: String }, // Id or public name, channel to send new disputes + dispute_channel: { type: String, unique: true, sparse: true }, // Id or public name, channel to send new disputes solvers: [usernameIdSchema], // users that are dispute solvers banned_users: [usernameIdSchema], // users that are banned from the community public: { type: Boolean, default: true }, diff --git a/scripts/migrate_duplicated_community_channels_and_groups.ts b/scripts/migrate_duplicated_community_channels_and_groups.ts new file mode 100644 index 00000000..0efde31a --- /dev/null +++ b/scripts/migrate_duplicated_community_channels_and_groups.ts @@ -0,0 +1,157 @@ +import 'dotenv/config'; +import { connect as mongoConnect } from '../db_connect'; +import { Community, User } from '../models'; +import { Telegraf } from 'telegraf'; +import { isGroupAdmin } from '../util'; +import * as readline from 'readline'; +import { logger } from '../logger'; + +const { BOT_TOKEN, MONGO_URI } = process.env; + +if (!BOT_TOKEN) { + console.error('BOT_TOKEN is missing'); + process.exit(1); +} + +if (!MONGO_URI) { + console.error('MONGO_URI is missing'); + process.exit(1); +} + +const bot = new Telegraf(BOT_TOKEN); + +export const runMigration = async () => { + const mongoose = mongoConnect(); + await new Promise((resolve, reject) => { + mongoose.connection.once('open', resolve); + mongoose.connection.on('error', reject); + }); + logger.info('Connected to database.'); + + const communities = await Community.find({}); + const groupCounts: Record = {}; + const disputeCounts: Record = {}; + const orderChannelCounts: Record = {}; + + // Count occurrences + communities.forEach(c => { + if (c.group) { + groupCounts[c.group] = (groupCounts[c.group] || 0) + 1; + } + if (c.dispute_channel) { + disputeCounts[c.dispute_channel] = + (disputeCounts[c.dispute_channel] || 0) + 1; + } + c.order_channels.forEach(ch => { + if (ch.name) { + orderChannelCounts[ch.name] = (orderChannelCounts[ch.name] || 0) + 1; + } + }); + }); + + const duplicateGroups = Object.keys(groupCounts).filter( + k => groupCounts[k] > 1, + ); + const duplicateDispute = Object.keys(disputeCounts).filter( + k => disputeCounts[k] > 1, + ); + const duplicateOrderChannels = Object.keys(orderChannelCounts).filter( + k => orderChannelCounts[k] > 1, + ); + + console.log(`Found ${duplicateGroups.length} duplicated groups.`); + console.log(`Found ${duplicateDispute.length} duplicated dispute channels.`); + console.log( + `Found ${duplicateOrderChannels.length} duplicated order channels.`, + ); + + const affectedCommunities = new Set(); + + for (const c of communities) { + const creator = await User.findById(c.creator_id); + + let modified = false; + + if (c.group && duplicateGroups.includes(c.group)) { + const isAdmin = creator + ? await isGroupAdmin(c.group, creator, bot.telegram) + : { success: false }; + if (!isAdmin.success) { + console.log(`Community ${c.name} (${c._id}) loses group ${c.group}`); + c.group = undefined as any; + modified = true; + } + } + + if (c.dispute_channel && duplicateDispute.includes(c.dispute_channel)) { + const isAdmin = creator + ? await isGroupAdmin(c.dispute_channel, creator, bot.telegram) + : { success: false }; + if (!isAdmin.success) { + console.log( + `Community ${c.name} (${c._id}) loses dispute_channel ${c.dispute_channel}`, + ); + c.dispute_channel = undefined as any; + modified = true; + } + } + + let channelsModified = false; + for (const ch of c.order_channels) { + if (ch.name && duplicateOrderChannels.includes(ch.name)) { + const isAdmin = creator + ? await isGroupAdmin(ch.name, creator, bot.telegram) + : { success: false }; + if (!isAdmin.success) { + channelsModified = true; + break; + } + } + } + + if (channelsModified) { + console.log( + `Community ${c.name} (${c._id}) loses all order_channels due to conflict in one of them`, + ); + c.order_channels = [] as any; + modified = true; + } + + if (modified) { + affectedCommunities.add(c._id.toString()); + } + } + + if (affectedCommunities.size === 0) { + console.log('No modifications needed. Exiting.'); + process.exit(0); + } + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question( + `\nType 'YES' to save changes to ${affectedCommunities.size} communities: `, + async answer => { + if (answer === 'YES') { + console.log('Saving changes...'); + for (const c of communities) { + if (affectedCommunities.has(c._id.toString())) { + await c.save(); + } + } + console.log('Done.'); + } else { + console.log('Aborted.'); + } + rl.close(); + process.exit(0); + }, + ); +}; + +if (require.main === module) { + runMigration().catch(console.error); +} diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts new file mode 100644 index 00000000..ee9f18ee --- /dev/null +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -0,0 +1,488 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noPreserveCache(); + +describe('Migration Script: migrate_duplicated_community_channels_and_groups', () => { + let sandbox: any; + let exitStub: any; + let communityFindStub: any; + let userFindByIdStub: any; + let isGroupAdminStub: any; + let connectStub: any; + let botStub: any; + let createInterfaceStub: any; + let mockRl: any; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Set process env locally + process.env.BOT_TOKEN = 'testtoken'; + process.env.MONGO_URI = 'mongodb://localhost:test'; + + exitStub = sandbox.stub(process, 'exit').callsFake((code: number) => { + throw new Error(`process.exit: ${code}`); + }); + const mockMongoose = { + connection: { + once: sandbox.stub().callsFake((event: string, cb: any) => { + if (event === 'open') cb(); // eslint-disable-line n/no-callback-literal + }), + on: sandbox.stub(), + }, + }; + connectStub = sandbox.stub().returns(mockMongoose); + + mockRl = { + question: sandbox.stub().callsFake((q: any, cb: any) => cb('YES')), // eslint-disable-line n/no-callback-literal + close: sandbox.stub(), + }; + createInterfaceStub = sandbox.stub().returns(mockRl); + + botStub = { + telegram: {}, + }; + + communityFindStub = sandbox.stub().resolves([]); + userFindByIdStub = sandbox.stub().resolves(null); + isGroupAdminStub = sandbox.stub().resolves({ success: false }); + // Silence console + sandbox.stub(console, 'log'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should exit with 0 if no duplicates found', async () => { + communityFindStub.resolves([ + { + _id: '1', + name: 'Community 1', + group: '@group1', + dispute_channel: '@disp1', + order_channels: [{ name: '@order1' }], + creator_id: 'user1', + }, + { + _id: '2', + name: 'Community 2', + group: '@group2', + dispute_channel: '@disp2', + order_channels: [{ name: '@order2' }], + creator_id: 'user2', + }, + ]); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { + Telegraf: sandbox.stub().returns(botStub), + }, + '../util': { + isGroupAdmin: isGroupAdminStub, + }, + readline: { + createInterface: createInterfaceStub, + }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + expect(connectStub.called).to.equal(true); + expect(exitStub.calledWith(0)).to.equal(true); + expect(createInterfaceStub.called).to.equal(false); + }); + + it('should unset fields and clear order_channels for non-admin on grouped dupes and save on YES', async () => { + const c1Save = sandbox.stub().resolves(); + const c2Save = sandbox.stub().resolves(); + + communityFindStub.resolves([ + { + _id: '1', + name: 'Community 1', + group: '@duplicate_group', + dispute_channel: '@duplicate_disp', + order_channels: [{ name: '@duplicate_order' }, { name: '@safe_order' }], + creator_id: 'user1', + save: c1Save, + }, + { + _id: '2', + name: 'Community 2', + group: '@duplicate_group', + dispute_channel: '@duplicate_disp', + order_channels: [{ name: '@duplicate_order' }], + creator_id: 'user2', + save: c2Save, + }, + ]); + + userFindByIdStub.withArgs('user1').resolves({ id: 'user1' }); + userFindByIdStub.withArgs('user2').resolves({ id: 'user2' }); + + // creator1 is admin, creator2 is not + isGroupAdminStub.callsFake(async (target: string, user: any) => { + if (user.id === 'user1') return { success: true }; + return { success: false }; + }); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { + Telegraf: sandbox.stub().returns(botStub), + }, + '../util': { + isGroupAdmin: isGroupAdminStub, + }, + readline: { + createInterface: createInterfaceStub, + }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + expect(createInterfaceStub.calledOnce).to.equal(true); + expect(c1Save.called).to.equal(false); // Was not modified because user1 is admin + expect(c2Save.calledOnce).to.equal(true); // Was modified because user2 is not admin + + const communities = await communityFindStub.firstCall.returnValue; + const resolvedC2 = communities[1]; + + expect(resolvedC2.group).to.equal(undefined); + expect(resolvedC2.dispute_channel).to.equal(undefined); + expect(resolvedC2.order_channels.length).to.equal(0); + expect(exitStub.calledWith(0)).to.equal(true); + }); + it('should handle more than two duplicates and clear multiple communities', async () => { + const c1Save = sandbox.stub().resolves(); + const c2Save = sandbox.stub().resolves(); + const c3Save = sandbox.stub().resolves(); + + communityFindStub.resolves([ + { + _id: '1', + name: 'C1', + group: '@dupe', + creator_id: 'u1', + save: c1Save, + order_channels: [{ name: '@c1' }], + }, + { + _id: '2', + name: 'C2', + group: '@dupe', + creator_id: 'u2', + save: c2Save, + order_channels: [{ name: '@c2' }], + }, + { + _id: '3', + name: 'C3', + group: '@dupe', + creator_id: 'u3', + save: c3Save, + order_channels: [{ name: '@c3' }], + }, + ]); + + userFindByIdStub.withArgs('u1').resolves({ id: 'u1' }); + userFindByIdStub.withArgs('u2').resolves({ id: 'u2' }); + userFindByIdStub.withArgs('u3').resolves({ id: 'u3' }); + + // Only u1 is admin + isGroupAdminStub.callsFake(async (target: string, user: any) => { + return { success: user.id === 'u1' }; + }); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + readline: { createInterface: createInterfaceStub }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + expect(c1Save.called).to.equal(false); + expect(c2Save.calledOnce).to.equal(true); + expect(c3Save.calledOnce).to.equal(true); + }); + + it('should NOT save changes if admin answers NO', async () => { + const cSave = sandbox.stub().resolves(); + communityFindStub.resolves([ + { + _id: '1', + name: 'C1', + group: '@d', + creator_id: 'u1', + order_channels: [{ name: '@o1' }], + save: cSave, + }, + { + _id: '2', + name: 'C2', + group: '@d', + creator_id: 'u2', + order_channels: [{ name: '@o2' }], + save: cSave, + }, + ]); + + // User u2 is not admin, so C2 needs change + isGroupAdminStub.resolves({ success: false }); + mockRl.question.callsFake((q: any, cb: any) => cb('NO')); // eslint-disable-line n/no-callback-literal + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + readline: { createInterface: createInterfaceStub }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + expect(cSave.called).to.equal(false); + }); + + it('should only clear group if only group is duplicated', async () => { + const c2Save = sandbox.stub().resolves(); + communityFindStub.resolves([ + { + _id: '1', + group: '@g', + dispute_channel: '@d1', + order_channels: [{ name: '@o1' }], + creator_id: 'u1', + save: sandbox.stub(), + }, + { + _id: '2', + group: '@g', + dispute_channel: '@d2', + order_channels: [{ name: '@o2' }], + creator_id: 'u2', + save: c2Save, + }, + ]); + isGroupAdminStub.resolves({ success: false }); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + readline: { createInterface: createInterfaceStub }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + const communities = await communityFindStub.firstCall.returnValue; + expect(communities[1].group).to.equal(undefined); + expect(communities[1].dispute_channel).to.be.equal('@d2'); // Remains untouched + expect(communities[1].order_channels[0].name).to.be.equal('@o2'); // Remains untouched + }); + + it('should only clear dispute_channel if only dispute_channel is duplicated', async () => { + const c2Save = sandbox.stub().resolves(); + communityFindStub.resolves([ + { + _id: '1', + group: '@g1', + dispute_channel: '@d', + order_channels: [{ name: '@o1' }], + creator_id: 'u1', + save: sandbox.stub(), + }, + { + _id: '2', + group: '@g2', + dispute_channel: '@d', + order_channels: [{ name: '@o2' }], + creator_id: 'u2', + save: c2Save, + }, + ]); + isGroupAdminStub.resolves({ success: false }); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + readline: { createInterface: createInterfaceStub }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + const communities = await communityFindStub.firstCall.returnValue; + expect(communities[1].dispute_channel).to.equal(undefined); + expect(communities[1].group).to.be.equal('@g2'); + }); + + it('should only clear order_channels if only order_channels are duplicated', async () => { + const c2Save = sandbox.stub().resolves(); + communityFindStub.resolves([ + { + _id: '1', + group: '@g1', + dispute_channel: '@d1', + order_channels: [{ name: '@o' }], + creator_id: 'u1', + save: sandbox.stub(), + }, + { + _id: '2', + name: 'C2', + group: '@g2', + dispute_channel: '@d2', + order_channels: [{ name: '@o' }, { name: '@safe' }], + creator_id: 'u2', + save: c2Save, + }, + ]); + isGroupAdminStub.resolves({ success: false }); + + const { runMigration } = proxyquire( + '../../scripts/migrate_duplicated_community_channels_and_groups', + { + '../db_connect': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub }, + }, + telegraf: { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + readline: { createInterface: createInterfaceStub }, + '../logger': { + logger: { + info: sandbox.stub(), + error: sandbox.stub(), + warn: sandbox.stub(), + }, + }, + }, + ); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + const communities = await communityFindStub.firstCall.returnValue; + expect(communities[1].order_channels.length).to.equal(0); + expect(communities[1].group).to.be.equal('@g2'); + expect(communities[1].dispute_channel).to.be.equal('@d2'); + }); +}); + +export {}; diff --git a/util/index.ts b/util/index.ts index d484dec6..aeda27f7 100644 --- a/util/index.ts +++ b/util/index.ts @@ -8,7 +8,7 @@ import { Telegram } from 'telegraf'; import axios from 'axios'; import fiatJson from './fiat.json'; import languagesJson from './languages.json'; -import { Order, Community } from '../models'; +import { Order, Community, User } from '../models'; import { logger } from '../logger'; import QRCode from 'qrcode'; import { Image, createCanvas } from 'canvas'; @@ -318,8 +318,8 @@ const deleteOrderFromChannel = async (order: IOrder, telegram: Telegram) => { } }; -const getOrderChannel = async (order: IOrder) => { - let channel = process.env.CHANNEL; +const getOrderChannel = async (order: IOrder, bot?: Telegram) => { + let channel; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); if (!community) { @@ -328,12 +328,32 @@ const getOrderChannel = async (order: IOrder) => { if (community.order_channels.length === 1) { channel = community.order_channels[0].name; } else { - community.order_channels.forEach(async (c: IOrderChannel) => { + community.order_channels.forEach((c: IOrderChannel) => { if (c.type === order.type) { channel = c.name; } }); } + const communityOwner = await User.findById(community.creator_id); + if (!communityOwner) { + return undefined; + } + + if (bot && channel) { + // Validate order channel if the caller of this function passed the bot instance to perform the validation + // If it was not passed as a parameter the order channel can be trusted because its for ui purposes (listorders for example) + // This validation is performed lazily when publishing the order to the community order channel + const isChannelOk = await isGroupAdmin(channel, communityOwner, bot); + if (!isChannelOk.success) { + logger.error( + `Order channel validation failed for community ${community._id}`, + ); + return undefined; + } + } + } else { + // no community order / order in the global community + channel = process.env.CHANNEL; } return channel;