From fa777c337ef62c0679efa954df967630589b94b1 Mon Sep 17 00:00:00 2001 From: lucas Date: Tue, 12 May 2026 00:34:55 -0300 Subject: [PATCH 01/16] * Add unique constraints on community group, dispute channel and order channels * Revalidate community order channel ownership by community owner lazily, at order creation time --- bot/messages.ts | 16 ++++++++++++---- models/community.ts | 6 +++--- util/index.ts | 18 ++++++++++++++++-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/bot/messages.ts b/bot/messages.ts index 17354be0..e58cea4e 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -730,8 +730,12 @@ 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) { + order.status = 'CLOSED'; + await order.save(); + throw new Error('channel is undefined'); + } // Get the community language if available let communityI18n = i18n; @@ -782,8 +786,12 @@ 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) { + order.status = 'CLOSED'; + await order.save(); + throw new Error('channel is undefined'); + } // Get the community language if available let communityI18n = i18n; diff --git a/models/community.ts b/models/community.ts index 8b1b8bb2..d9cdeb17 100644 --- a/models/community.ts +++ b/models/community.ts @@ -17,7 +17,7 @@ export interface IOrderChannel extends Document { } const OrderChannelSchema = new Schema({ - name: { type: String, required: true, trim: true }, + name: { type: String, required: true, unique: true }, type: { type: String, enum: ['buy', 'sell', 'mixed'], @@ -64,7 +64,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 }, // group Id or public name order_channels: { // array of Id or public name of channels type: [OrderChannelSchema], @@ -73,7 +73,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}, // 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/util/index.ts b/util/index.ts index d484dec6..d75dcacd 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,7 +318,7 @@ const deleteOrderFromChannel = async (order: IOrder, telegram: Telegram) => { } }; -const getOrderChannel = async (order: IOrder) => { +const getOrderChannel = async (order: IOrder, bot?: Telegram) => { let channel = process.env.CHANNEL; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); @@ -334,6 +334,20 @@ const getOrderChannel = async (order: IOrder) => { } }); } + 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) { + return undefined; + } + } } return channel; From 08a3f6c5131432c7df8cbada7e82f03d089533f8 Mon Sep 17 00:00:00 2001 From: lucas Date: Tue, 12 May 2026 00:39:49 -0300 Subject: [PATCH 02/16] Code formatting --- models/community.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/community.ts b/models/community.ts index d9cdeb17..4e6caf72 100644 --- a/models/community.ts +++ b/models/community.ts @@ -73,7 +73,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, unique: true}, // Id or public name, channel to send new disputes + dispute_channel: { type: String, unique: 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 }, From bff1b51fb80d3ff634b77f2823a172fa88e6a393 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 13 May 2026 23:27:56 -0300 Subject: [PATCH 03/16] add migration script to resolve community channel and group duplicates and update schema to support sparse unique indexes --- models/community.ts | 6 +- ...uplicated_community_channels_and_groups.ts | 130 +++++++++++++ ...ated_community_channels_and_groups.spec.ts | 172 ++++++++++++++++++ 3 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 scripts/migrate_duplicated_community_channels_and_groups.ts create mode 100644 tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts diff --git a/models/community.ts b/models/community.ts index 4e6caf72..eca8d578 100644 --- a/models/community.ts +++ b/models/community.ts @@ -17,7 +17,7 @@ export interface IOrderChannel extends Document { } const OrderChannelSchema = new Schema({ - name: { type: String, required: true, unique: true }, + name: { type: String, unique: true, sparse: true }, type: { type: String, enum: ['buy', 'sell', 'mixed'], @@ -64,7 +64,7 @@ const CommunitySchema = new Schema({ required: true, }, creator_id: { type: String }, - group: { type: String, unique: 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], @@ -73,7 +73,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, unique: true }, // 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..9aa43d6e --- /dev/null +++ b/scripts/migrate_duplicated_community_channels_and_groups.ts @@ -0,0 +1,130 @@ +import 'dotenv/config'; +import { connect } from 'mongoose'; +import { Community, User } from '../models'; +import { Telegraf } from 'telegraf'; +import { isGroupAdmin } from '../util'; +import * as readline from 'readline'; + +const { BOT_TOKEN, DB_URI } = process.env; + +if (!BOT_TOKEN) { + console.error('BOT_TOKEN is missing'); + process.exit(1); +} + +if (!DB_URI) { + console.error('DB_URI is missing'); + process.exit(1); +} + +const bot = new Telegraf(BOT_TOKEN); + +export const runMigration = async () => { + await connect(DB_URI); + console.log('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..3cd5d55c --- /dev/null +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -0,0 +1,172 @@ +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 createInterfaceStub: any; + let mockRl: any; + let botStub: any; + let consoleLogStub: any; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Set process env locally + process.env.BOT_TOKEN = 'testtoken'; + process.env.DB_URI = 'mongodb://localhost:test'; + + exitStub = sandbox.stub(process, 'exit').callsFake((code: number) => { + throw new Error(`process.exit: ${code}`); + }); + connectStub = sandbox.stub().resolves(); + + mockRl = { + question: sandbox.stub().callsFake((q: any, cb: any) => cb('YES')), + 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 }); + // Print through original console + consoleLogStub = sandbox.stub(console, 'log').callsFake((...args: any[]) => { + console.info('STUBBED LOG:', ...args); + }); + }); + + 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', { + 'mongoose': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub } + }, + 'telegraf': { + Telegraf: sandbox.stub().returns(botStub) + }, + '../util': { + isGroupAdmin: isGroupAdminStub + }, + 'readline': { + createInterface: createInterfaceStub + } + }); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + console.info('Test 1 finished runMigration'); + + expect(connectStub.called).to.be.true; + expect(exitStub.calledWith(0)).to.be.true; + expect(createInterfaceStub.called).to.be.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', { + 'mongoose': { connect: connectStub }, + '../models': { + Community: { find: communityFindStub }, + User: { findById: userFindByIdStub } + }, + 'telegraf': { + Telegraf: sandbox.stub().returns(botStub) + }, + '../util': { + isGroupAdmin: isGroupAdminStub + }, + 'readline': { + createInterface: createInterfaceStub + } + }); + + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } + + expect(createInterfaceStub.calledOnce).to.be.true; + expect(c1Save.called).to.be.false; // Was not modified because user1 is admin + expect(c2Save.calledOnce).to.be.true; // Was modified because user2 is not admin + + const communities = await communityFindStub.firstCall.returnValue; + const resolvedC2 = communities[1]; + + expect(resolvedC2.group).to.be.undefined; + expect(resolvedC2.dispute_channel).to.be.undefined; + expect(resolvedC2.order_channels).to.deep.equal([]); + expect(exitStub.calledWith(0)).to.be.true; + }); +}); + +export {}; From 4f5ffe0b3799930b8ffff552acd4054d981b4375 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 13 May 2026 23:32:42 -0300 Subject: [PATCH 04/16] Add test cases for community migration script --- ...ated_community_channels_and_groups.spec.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index 3cd5d55c..a2d81cb6 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -167,6 +167,135 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( expect(resolvedC2.order_channels).to.deep.equal([]); expect(exitStub.calledWith(0)).to.be.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: [] }, + { _id: '2', name: 'C2', group: '@dupe', creator_id: 'u2', save: c2Save, order_channels: [] }, + { _id: '3', name: 'C3', group: '@dupe', creator_id: 'u3', save: c3Save, order_channels: [] } + ]); + + 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', { + 'mongoose': { connect: connectStub }, + '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, + 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + 'readline': { createInterface: createInterfaceStub } + }); + + try { await runMigration(); } catch (e: any) { if (!e.message.includes('process.exit')) throw e; } + + expect(c1Save.called).to.be.false; + expect(c2Save.calledOnce).to.be.true; + expect(c3Save.calledOnce).to.be.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: [], save: cSave }, + { _id: '2', name: 'C2', group: '@d', creator_id: 'u2', order_channels: [], save: cSave } + ]); + + // User u2 is not admin, so C2 needs change + isGroupAdminStub.resolves({ success: false }); + mockRl.question.callsFake((q: any, cb: any) => cb('NO')); + + const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { + 'mongoose': { connect: connectStub }, + '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, + 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + 'readline': { createInterface: createInterfaceStub } + }); + + try { await runMigration(); } catch (e: any) { if (!e.message.includes('process.exit')) throw e; } + + expect(cSave.called).to.be.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', { + 'mongoose': { connect: connectStub }, + '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, + 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + 'readline': { createInterface: createInterfaceStub } + }); + + 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.be.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', { + 'mongoose': { connect: connectStub }, + '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, + 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + 'readline': { createInterface: createInterfaceStub } + }); + + 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.be.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', group: '@g2', dispute_channel: '@d2', order_channels: [{name: '@o'}], creator_id: 'u2', save: c2Save } + ]); + isGroupAdminStub.resolves({ success: false }); + + const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { + 'mongoose': { connect: connectStub }, + '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, + 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, + '../util': { isGroupAdmin: isGroupAdminStub }, + 'readline': { createInterface: createInterfaceStub } + }); + + 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).to.deep.equal([]); + expect(communities[1].group).to.be.equal('@g2'); + expect(communities[1].dispute_channel).to.be.equal('@d2'); + }); }); export {}; From 26c23e6a69df5e7156a5183869495a4defffeba4 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 13 May 2026 23:38:50 -0300 Subject: [PATCH 05/16] Remove redundant logs from migration script unit tests --- ...igrate_duplicated_community_channels_and_groups.spec.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index a2d81cb6..07c86944 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -39,10 +39,8 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( communityFindStub = sandbox.stub().resolves([]); userFindByIdStub = sandbox.stub().resolves(null); isGroupAdminStub = sandbox.stub().resolves({ success: false }); - // Print through original console - consoleLogStub = sandbox.stub(console, 'log').callsFake((...args: any[]) => { - console.info('STUBBED LOG:', ...args); - }); + // Silence console + consoleLogStub = sandbox.stub(console, 'log'); }); afterEach(() => { @@ -91,7 +89,6 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( } catch (e: any) { if (!e.message.includes('process.exit')) throw e; } - console.info('Test 1 finished runMigration'); expect(connectStub.called).to.be.true; expect(exitStub.calledWith(0)).to.be.true; From 67c71c6ae78a73f631c852ea92766a22a76e9e7a Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 14 May 2026 00:08:57 -0300 Subject: [PATCH 06/16] Use db_connect to connect to mongoose in the migration script --- ...uplicated_community_channels_and_groups.ts | 99 +++-- ...ated_community_channels_and_groups.spec.ts | 372 +++++++++++++----- 2 files changed, 344 insertions(+), 127 deletions(-) diff --git a/scripts/migrate_duplicated_community_channels_and_groups.ts b/scripts/migrate_duplicated_community_channels_and_groups.ts index 9aa43d6e..0efde31a 100644 --- a/scripts/migrate_duplicated_community_channels_and_groups.ts +++ b/scripts/migrate_duplicated_community_channels_and_groups.ts @@ -1,27 +1,32 @@ import 'dotenv/config'; -import { connect } from 'mongoose'; +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, DB_URI } = process.env; +const { BOT_TOKEN, MONGO_URI } = process.env; if (!BOT_TOKEN) { console.error('BOT_TOKEN is missing'); process.exit(1); } -if (!DB_URI) { - console.error('DB_URI is missing'); +if (!MONGO_URI) { + console.error('MONGO_URI is missing'); process.exit(1); } const bot = new Telegraf(BOT_TOKEN); export const runMigration = async () => { - await connect(DB_URI); - console.log('Connected to database.'); + 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 = {}; @@ -31,25 +36,34 @@ export const runMigration = async () => { // Count occurrences communities.forEach(c => { if (c.group) { - groupCounts[c.group] = (groupCounts[c.group] || 0) + 1; + groupCounts[c.group] = (groupCounts[c.group] || 0) + 1; } if (c.dispute_channel) { - disputeCounts[c.dispute_channel] = (disputeCounts[c.dispute_channel] || 0) + 1; + 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; - } + 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); + 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.`); + console.log( + `Found ${duplicateOrderChannels.length} duplicated order channels.`, + ); const affectedCommunities = new Set(); @@ -59,7 +73,9 @@ export const runMigration = async () => { let modified = false; if (c.group && duplicateGroups.includes(c.group)) { - const isAdmin = creator ? await isGroupAdmin(c.group, creator, bot.telegram) : { success: false }; + 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; @@ -68,9 +84,13 @@ export const runMigration = async () => { } if (c.dispute_channel && duplicateDispute.includes(c.dispute_channel)) { - const isAdmin = creator ? await isGroupAdmin(c.dispute_channel, creator, bot.telegram) : { success: false }; + 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}`); + console.log( + `Community ${c.name} (${c._id}) loses dispute_channel ${c.dispute_channel}`, + ); c.dispute_channel = undefined as any; modified = true; } @@ -79,16 +99,20 @@ export const runMigration = async () => { 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 }; + const isAdmin = creator + ? await isGroupAdmin(ch.name, creator, bot.telegram) + : { success: false }; if (!isAdmin.success) { - channelsModified = true; - break; + channelsModified = true; + break; } } } if (channelsModified) { - console.log(`Community ${c.name} (${c._id}) loses all order_channels due to conflict in one of them`); + 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; } @@ -105,24 +129,27 @@ export const runMigration = async () => { const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + 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(); + 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.'); } - console.log('Done.'); - } else { - console.log('Aborted.'); - } - rl.close(); - process.exit(0); - }); + rl.close(); + process.exit(0); + }, + ); }; if (require.main === module) { diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index 07c86944..92c778e4 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -19,12 +19,20 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( // Set process env locally process.env.BOT_TOKEN = 'testtoken'; - process.env.DB_URI = 'mongodb://localhost:test'; + process.env.MONGO_URI = 'mongodb://localhost:test'; exitStub = sandbox.stub(process, 'exit').callsFake((code: number) => { throw new Error(`process.exit: ${code}`); }); - connectStub = sandbox.stub().resolves(); + const mockMongoose = { + connection: { + once: sandbox.stub().callsFake((event: string, cb: any) => { + if (event === 'open') cb(); + }), + on: sandbox.stub(), + }, + }; + connectStub = sandbox.stub().returns(mockMongoose); mockRl = { question: sandbox.stub().callsFake((q: any, cb: any) => cb('YES')), @@ -33,7 +41,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( createInterfaceStub = sandbox.stub().returns(mockRl); botStub = { - telegram: {} + telegram: {}, }; communityFindStub = sandbox.stub().resolves([]); @@ -55,7 +63,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( group: '@group1', dispute_channel: '@disp1', order_channels: [{ name: '@order1' }], - creator_id: 'user1' + creator_id: 'user1', }, { _id: '2', @@ -63,26 +71,36 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( group: '@group2', dispute_channel: '@disp2', order_channels: [{ name: '@order2' }], - creator_id: 'user2' - } + creator_id: 'user2', + }, ]); - const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { - 'mongoose': { connect: connectStub }, - '../models': { - Community: { find: communityFindStub }, - User: { findById: userFindByIdStub } - }, - 'telegraf': { - Telegraf: sandbox.stub().returns(botStub) - }, - '../util': { - isGroupAdmin: isGroupAdminStub + 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(), + }, + }, }, - 'readline': { - createInterface: createInterfaceStub - } - }); + ); try { await runMigration(); @@ -107,7 +125,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( dispute_channel: '@duplicate_disp', order_channels: [{ name: '@duplicate_order' }, { name: '@safe_order' }], creator_id: 'user1', - save: c1Save + save: c1Save, }, { _id: '2', @@ -116,8 +134,8 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( dispute_channel: '@duplicate_disp', order_channels: [{ name: '@duplicate_order' }], creator_id: 'user2', - save: c2Save - } + save: c2Save, + }, ]); userFindByIdStub.withArgs('user1').resolves({ id: 'user1' }); @@ -129,22 +147,32 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( return { success: false }; }); - const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { - 'mongoose': { connect: connectStub }, - '../models': { - Community: { find: communityFindStub }, - User: { findById: userFindByIdStub } - }, - 'telegraf': { - Telegraf: sandbox.stub().returns(botStub) - }, - '../util': { - isGroupAdmin: isGroupAdminStub + 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(), + }, + }, }, - 'readline': { - createInterface: createInterfaceStub - } - }); + ); try { await runMigration(); @@ -170,9 +198,30 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( const c3Save = sandbox.stub().resolves(); communityFindStub.resolves([ - { _id: '1', name: 'C1', group: '@dupe', creator_id: 'u1', save: c1Save, order_channels: [] }, - { _id: '2', name: 'C2', group: '@dupe', creator_id: 'u2', save: c2Save, order_channels: [] }, - { _id: '3', name: 'C3', group: '@dupe', creator_id: 'u3', save: c3Save, order_channels: [] } + { + _id: '1', + name: 'C1', + group: '@dupe', + creator_id: 'u1', + save: c1Save, + order_channels: [], + }, + { + _id: '2', + name: 'C2', + group: '@dupe', + creator_id: 'u2', + save: c2Save, + order_channels: [], + }, + { + _id: '3', + name: 'C3', + group: '@dupe', + creator_id: 'u3', + save: c3Save, + order_channels: [], + }, ]); userFindByIdStub.withArgs('u1').resolves({ id: 'u1' }); @@ -184,15 +233,32 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( return { success: user.id === 'u1' }; }); - const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { - 'mongoose': { connect: connectStub }, - '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, - 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, - '../util': { isGroupAdmin: isGroupAdminStub }, - 'readline': { createInterface: createInterfaceStub } - }); + 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; } + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } expect(c1Save.called).to.be.false; expect(c2Save.calledOnce).to.be.true; @@ -202,23 +268,54 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( 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: [], save: cSave }, - { _id: '2', name: 'C2', group: '@d', creator_id: 'u2', order_channels: [], save: cSave } + { + _id: '1', + name: 'C1', + group: '@d', + creator_id: 'u1', + order_channels: [], + save: cSave, + }, + { + _id: '2', + name: 'C2', + group: '@d', + creator_id: 'u2', + order_channels: [], + save: cSave, + }, ]); - + // User u2 is not admin, so C2 needs change isGroupAdminStub.resolves({ success: false }); mockRl.question.callsFake((q: any, cb: any) => cb('NO')); - const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { - 'mongoose': { connect: connectStub }, - '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, - 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, - '../util': { isGroupAdmin: isGroupAdminStub }, - 'readline': { createInterface: createInterfaceStub } - }); + 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; } + try { + await runMigration(); + } catch (e: any) { + if (!e.message.includes('process.exit')) throw e; + } expect(cSave.called).to.be.false; }); @@ -226,20 +323,51 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( 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 } + { + _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', { - 'mongoose': { connect: connectStub }, - '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, - 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, - '../util': { isGroupAdmin: isGroupAdminStub }, - 'readline': { createInterface: createInterfaceStub } - }); + 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; } + 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.be.undefined; @@ -250,20 +378,51 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( 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 } + { + _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', { - 'mongoose': { connect: connectStub }, - '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, - 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, - '../util': { isGroupAdmin: isGroupAdminStub }, - 'readline': { createInterface: createInterfaceStub } - }); + 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; } + 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.be.undefined; @@ -273,20 +432,51 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( 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', group: '@g2', dispute_channel: '@d2', order_channels: [{name: '@o'}], creator_id: 'u2', save: c2Save } + { + _id: '1', + group: '@g1', + dispute_channel: '@d1', + order_channels: [{ name: '@o' }], + creator_id: 'u1', + save: sandbox.stub(), + }, + { + _id: '2', + group: '@g2', + dispute_channel: '@d2', + order_channels: [{ name: '@o' }], + creator_id: 'u2', + save: c2Save, + }, ]); isGroupAdminStub.resolves({ success: false }); - const { runMigration } = proxyquire('../../scripts/migrate_duplicated_community_channels_and_groups', { - 'mongoose': { connect: connectStub }, - '../models': { Community: { find: communityFindStub }, User: { findById: userFindByIdStub } }, - 'telegraf': { Telegraf: sandbox.stub().returns(botStub) }, - '../util': { isGroupAdmin: isGroupAdminStub }, - 'readline': { createInterface: createInterfaceStub } - }); + 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; } + 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).to.deep.equal([]); From d4e9b60b04993153410af687b42224e4f77a295c Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 14 May 2026 00:15:37 -0300 Subject: [PATCH 07/16] Set channel name to undefined when community owner is not admin --- scripts/migrate_duplicated_community_channels_and_groups.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/migrate_duplicated_community_channels_and_groups.ts b/scripts/migrate_duplicated_community_channels_and_groups.ts index 0efde31a..8e9f9415 100644 --- a/scripts/migrate_duplicated_community_channels_and_groups.ts +++ b/scripts/migrate_duplicated_community_channels_and_groups.ts @@ -113,7 +113,9 @@ export const runMigration = async () => { console.log( `Community ${c.name} (${c._id}) loses all order_channels due to conflict in one of them`, ); - c.order_channels = [] as any; + for (const ch of c.order_channels) { + ch.name = undefined as any; + } modified = true; } From 80a063a6eeaf37be7b4a5acd25d44e57cd72d6aa Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 14 May 2026 00:31:12 -0300 Subject: [PATCH 08/16] Fix lint errors --- ...ated_community_channels_and_groups.spec.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index 92c778e4..5dbe1416 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -9,10 +9,9 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( let userFindByIdStub: any; let isGroupAdminStub: any; let connectStub: any; + let botStub: any; let createInterfaceStub: any; let mockRl: any; - let botStub: any; - let consoleLogStub: any; beforeEach(() => { sandbox = sinon.createSandbox(); @@ -27,7 +26,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( const mockMongoose = { connection: { once: sandbox.stub().callsFake((event: string, cb: any) => { - if (event === 'open') cb(); + if (event === 'open') cb(); // eslint-disable-line n/no-callback-literal }), on: sandbox.stub(), }, @@ -35,7 +34,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( connectStub = sandbox.stub().returns(mockMongoose); mockRl = { - question: sandbox.stub().callsFake((q: any, cb: any) => cb('YES')), + 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); @@ -48,7 +47,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( userFindByIdStub = sandbox.stub().resolves(null); isGroupAdminStub = sandbox.stub().resolves({ success: false }); // Silence console - consoleLogStub = sandbox.stub(console, 'log'); + sandbox.stub(console, 'log'); }); afterEach(() => { @@ -108,9 +107,9 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( if (!e.message.includes('process.exit')) throw e; } - expect(connectStub.called).to.be.true; - expect(exitStub.calledWith(0)).to.be.true; - expect(createInterfaceStub.called).to.be.false; + 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 () => { @@ -180,17 +179,17 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( if (!e.message.includes('process.exit')) throw e; } - expect(createInterfaceStub.calledOnce).to.be.true; - expect(c1Save.called).to.be.false; // Was not modified because user1 is admin - expect(c2Save.calledOnce).to.be.true; // Was modified because user2 is not admin + 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.be.undefined; - expect(resolvedC2.dispute_channel).to.be.undefined; - expect(resolvedC2.order_channels).to.deep.equal([]); - expect(exitStub.calledWith(0)).to.be.true; + expect(resolvedC2.group).to.equal(undefined); + expect(resolvedC2.dispute_channel).to.equal(undefined); + expect(resolvedC2.order_channels[0].name).to.equal(undefined); + expect(exitStub.calledWith(0)).to.equal(true); }); it('should handle more than two duplicates and clear multiple communities', async () => { const c1Save = sandbox.stub().resolves(); @@ -260,9 +259,9 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( if (!e.message.includes('process.exit')) throw e; } - expect(c1Save.called).to.be.false; - expect(c2Save.calledOnce).to.be.true; - expect(c3Save.calledOnce).to.be.true; + 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 () => { @@ -288,7 +287,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( // User u2 is not admin, so C2 needs change isGroupAdminStub.resolves({ success: false }); - mockRl.question.callsFake((q: any, cb: any) => cb('NO')); + 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', @@ -317,7 +316,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( if (!e.message.includes('process.exit')) throw e; } - expect(cSave.called).to.be.false; + expect(cSave.called).to.equal(false); }); it('should only clear group if only group is duplicated', async () => { @@ -370,7 +369,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( } const communities = await communityFindStub.firstCall.returnValue; - expect(communities[1].group).to.be.undefined; + 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 }); @@ -425,7 +424,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( } const communities = await communityFindStub.firstCall.returnValue; - expect(communities[1].dispute_channel).to.be.undefined; + expect(communities[1].dispute_channel).to.equal(undefined); expect(communities[1].group).to.be.equal('@g2'); }); @@ -479,7 +478,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( } const communities = await communityFindStub.firstCall.returnValue; - expect(communities[1].order_channels).to.deep.equal([]); + expect(communities[1].order_channels[0].name).to.equal(undefined); expect(communities[1].group).to.be.equal('@g2'); expect(communities[1].dispute_channel).to.be.equal('@d2'); }); From 2ce96f9f903e5120a72f27426483565a99448353 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 14 May 2026 00:38:49 -0300 Subject: [PATCH 09/16] Test should reflect script behaviour. It should set order channel name to undefined, not an empty array --- ...uplicated_community_channels_and_groups.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index 5dbe1416..78d796c5 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -203,7 +203,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( group: '@dupe', creator_id: 'u1', save: c1Save, - order_channels: [], + order_channels: [{ name: '@c1' }], }, { _id: '2', @@ -211,7 +211,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( group: '@dupe', creator_id: 'u2', save: c2Save, - order_channels: [], + order_channels: [{ name: '@c2' }], }, { _id: '3', @@ -219,7 +219,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( group: '@dupe', creator_id: 'u3', save: c3Save, - order_channels: [], + order_channels: [{ name: '@c3' }], }, ]); @@ -272,7 +272,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( name: 'C1', group: '@d', creator_id: 'u1', - order_channels: [], + order_channels: [{ name: '@o1' }], save: cSave, }, { @@ -280,7 +280,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( name: 'C2', group: '@d', creator_id: 'u2', - order_channels: [], + order_channels: [{ name: '@o2' }], save: cSave, }, ]); @@ -441,9 +441,10 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( }, { _id: '2', + name: 'C2', group: '@g2', dispute_channel: '@d2', - order_channels: [{ name: '@o' }], + order_channels: [{ name: '@o' }, { name: '@safe' }], creator_id: 'u2', save: c2Save, }, @@ -479,6 +480,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( const communities = await communityFindStub.firstCall.returnValue; expect(communities[1].order_channels[0].name).to.equal(undefined); + expect(communities[1].order_channels[1].name).to.equal(undefined); expect(communities[1].group).to.be.equal('@g2'); expect(communities[1].dispute_channel).to.be.equal('@d2'); }); From 518f5ad97c3d59afab7b172256c104f97aed0aa6 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 15:56:10 -0300 Subject: [PATCH 10/16] Send a message to the user if community channel validations fails --- bot/messages.ts | 12 ++++++++++-- locales/de.yaml | 1 + locales/en.yaml | 1 + locales/es.yaml | 1 + locales/fa.yaml | 1 + locales/fr.yaml | 1 + locales/it.yaml | 1 + locales/ko.yaml | 1 + locales/pt.yaml | 1 + locales/ru.yaml | 1 + locales/uk.yaml | 1 + 11 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bot/messages.ts b/bot/messages.ts index e58cea4e..1a60f9a6 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -732,9 +732,13 @@ const publishBuyOrderMessage = async ( const channel = await getOrderChannel(order, bot.telegram); if (channel === undefined) { + await bot.telegram.sendMessage( + user.tg_id, + i18n.t('order_channel_validation_failed') + ); order.status = 'CLOSED'; await order.save(); - throw new Error('channel is undefined'); + return; } // Get the community language if available @@ -788,9 +792,13 @@ const publishSellOrderMessage = async ( publishMessage += `:${order._id}:`; const channel = await getOrderChannel(order, ctx.telegram); if (channel === undefined) { + await ctx.telegram.sendMessage( + user.tg_id, + i18n.t('order_channel_validation_failed') + ); order.status = 'CLOSED'; await order.save(); - throw new Error('channel is undefined'); + return; } // Get the community language if available diff --git a/locales/de.yaml b/locales/de.yaml index 8aa1b665..c4b28ba1 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -707,3 +707,4 @@ 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." diff --git a/locales/en.yaml b/locales/en.yaml index c12c9407..9093275a 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -712,3 +712,4 @@ 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." diff --git a/locales/es.yaml b/locales/es.yaml index b77314c4..feb0d4f6 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -709,3 +709,4 @@ 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." diff --git a/locales/fa.yaml b/locales/fa.yaml index 824acd93..2b0af864 100644 --- a/locales/fa.yaml +++ b/locales/fa.yaml @@ -806,3 +806,4 @@ unblock_failed: "خطا در رفع مسدودیت کاربر" check_solvers: اجتماع شما ${communityName} هیچ داوری ندارد. لطفاً برای جلوگیری از غیرفعال شدن اجتماع، تا ${remainingDays} روز آینده حداقل یک داور به آن اضافه کنید. check_solvers_last_warning: اجتماع شما ${communityName} هیچ داوری ندارد. برای جلوگیری از غیرفعال شدن اجتماع، امروز حداقل یک داور به آن اضافه کنید. image_processing_error: هنگام پردازش تصویر با خطایی مواجه شدیم، لطفاً چند دقیقه صبر کرده و دوباره امتحان کنید. +order_channel_validation_failed: "امکان انتشار سفارش شما وجود ندارد. سعی کنید انجمن پیش‌فرض را با /setcomm off غیرفعال کنید یا از مدیر انجمن خود بخواهید کانال‌ها و گروه‌های انجمن را مجدداً پیکربندی کند." diff --git a/locales/fr.yaml b/locales/fr.yaml index 38a4fad0..bb3f2351 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -706,3 +706,4 @@ 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é." diff --git a/locales/it.yaml b/locales/it.yaml index 2a0efcd5..83d12462 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -704,3 +704,4 @@ 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." diff --git a/locales/ko.yaml b/locales/ko.yaml index d2c15154..6d637b94 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -704,3 +704,4 @@ unblock_failed: "사용자 차단 해제 중 오류 발생" check_solvers: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 ${remainingDays}일 이내에 하나 이상 추가하세요. check_solvers_last_warning: ${communityName} 커뮤니티에 해결사가 없습니다. 커뮤니티가 비활성화되는 것을 방지하려면 오늘 하나 이상 추가하세요. image_processing_error: 이미지 처리에 오류가 발생했습니다. 몇 분 후에 다시 시도해 주세요. +order_channel_validation_failed: "주문을 등록할 수 없습니다. /setcomm off 로 기본 커뮤니티를 비활성화하거나, 커뮤니티 관리자에게 커뮤니티 채널 및 그룹을 재설정해 달라고 요청하세요." diff --git a/locales/pt.yaml b/locales/pt.yaml index c4c9c1a8..36e6bb28 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -706,3 +706,4 @@ 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." diff --git a/locales/ru.yaml b/locales/ru.yaml index 9404f3d8..6c1ecc21 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -707,3 +707,4 @@ unblock_failed: "Ошибка при разблокировке пользова check_solvers: В вашем сообществе ${communityName} нет решателей. Добавьте хотя бы одно в течение ${remainingDays} дн., чтобы сообщество не было отключено. check_solvers_last_warning: В вашем сообществе ${communityName} нет решателей. Пожалуйста, добавьте хотя бы один сегодня, чтобы предотвратить отключение сообщества. image_processing_error: У нас возникла ошибка при обработке изображения, пожалуйста, подождите несколько минут и попробуйте снова. +order_channel_validation_failed: "Не удалось опубликовать ваш ордер. Попробуйте отключить сообщество по умолчанию с помощью /setcomm off или попросите администратора сообщества перенастроить каналы и группы сообщества." diff --git a/locales/uk.yaml b/locales/uk.yaml index c3fcccc8..2b6eec1f 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -703,3 +703,4 @@ unblock_failed: "Помилка при розблокуванні користу check_solvers: У вашій спільноті ${communityName} немає розв’язувачів. Додайте принаймні одну протягом ${remainingDays} днів, щоб запобігти вимкненню спільноти. check_solvers_last_warning: У вашій спільноті ${communityName} немає розв’язувачів. Будь ласка, додайте принаймні одну сьогодні, щоб запобігти вимкненню спільноти. image_processing_error: У нас виникла помилка при обробці зображення, будь ласка, почекайте кілька хвилин і спробуйте знову. +order_channel_validation_failed: "Не вдалося опублікувати ваше замовлення. Спробуйте вимкнути спільноту за замовчуванням за допомогою /setcomm off або попросіть адміністратора спільноти переналаштувати канали та групи спільноти." From e4ae6e516e1c8ddb84893dbf04ed587f47565512 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 16:07:20 -0300 Subject: [PATCH 11/16] Wrap sendMessage in a try/catch block in the case community channel validation fails, so order state is always persisted --- bot/messages.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bot/messages.ts b/bot/messages.ts index 1a60f9a6..9b20c765 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -732,10 +732,14 @@ const publishBuyOrderMessage = async ( const channel = await getOrderChannel(order, bot.telegram); if (channel === undefined) { - await bot.telegram.sendMessage( - user.tg_id, - i18n.t('order_channel_validation_failed') - ); + 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; @@ -792,10 +796,14 @@ const publishSellOrderMessage = async ( publishMessage += `:${order._id}:`; const channel = await getOrderChannel(order, ctx.telegram); if (channel === undefined) { - await ctx.telegram.sendMessage( - user.tg_id, - i18n.t('order_channel_validation_failed') - ); + 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; From 9b1c8f311b4639c60c976dd9c045865f84f1bfe3 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 16:08:32 -0300 Subject: [PATCH 12/16] Code formatting --- bot/messages.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/messages.ts b/bot/messages.ts index 9b20c765..21cbd687 100644 --- a/bot/messages.ts +++ b/bot/messages.ts @@ -735,7 +735,7 @@ const publishBuyOrderMessage = async ( try { await bot.telegram.sendMessage( user.tg_id, - i18n.t('order_channel_validation_failed') + i18n.t('order_channel_validation_failed'), ); } catch (error) { logger.error(error); @@ -799,7 +799,7 @@ const publishSellOrderMessage = async ( try { await ctx.telegram.sendMessage( user.tg_id, - i18n.t('order_channel_validation_failed') + i18n.t('order_channel_validation_failed'), ); } catch (error) { logger.error(error); From d79306f44ecf3d2d7fc2ef34981276e6c4f483e9 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 19:44:17 -0300 Subject: [PATCH 13/16] Update migration script: communities with duplicated order channels should set order_channels to an empty array --- models/community.ts | 2 +- scripts/migrate_duplicated_community_channels_and_groups.ts | 4 +--- .../migrate_duplicated_community_channels_and_groups.spec.ts | 5 ++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/models/community.ts b/models/community.ts index 211a8463..cab69ae2 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 => { diff --git a/scripts/migrate_duplicated_community_channels_and_groups.ts b/scripts/migrate_duplicated_community_channels_and_groups.ts index 8e9f9415..0efde31a 100644 --- a/scripts/migrate_duplicated_community_channels_and_groups.ts +++ b/scripts/migrate_duplicated_community_channels_and_groups.ts @@ -113,9 +113,7 @@ export const runMigration = async () => { console.log( `Community ${c.name} (${c._id}) loses all order_channels due to conflict in one of them`, ); - for (const ch of c.order_channels) { - ch.name = undefined as any; - } + c.order_channels = [] as any; modified = true; } diff --git a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts index 78d796c5..ee9f18ee 100644 --- a/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts +++ b/tests/scripts/migrate_duplicated_community_channels_and_groups.spec.ts @@ -188,7 +188,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( expect(resolvedC2.group).to.equal(undefined); expect(resolvedC2.dispute_channel).to.equal(undefined); - expect(resolvedC2.order_channels[0].name).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 () => { @@ -479,8 +479,7 @@ describe('Migration Script: migrate_duplicated_community_channels_and_groups', ( } const communities = await communityFindStub.firstCall.returnValue; - expect(communities[1].order_channels[0].name).to.equal(undefined); - expect(communities[1].order_channels[1].name).to.equal(undefined); + 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'); }); From a6fa7504a9890538793f79851486c899cae81672 Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 19:59:45 -0300 Subject: [PATCH 14/16] Add logging when community channel validation fails, and remove the unnecesary async in the foreach loop that iterates to find the suitable order channel to publish the order --- util/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util/index.ts b/util/index.ts index d75dcacd..d38cf34e 100644 --- a/util/index.ts +++ b/util/index.ts @@ -328,7 +328,7 @@ const getOrderChannel = async (order: IOrder, bot?: Telegram) => { 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; } @@ -345,6 +345,9 @@ const getOrderChannel = async (order: IOrder, bot?: Telegram) => { // 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; } } From 673b23e5a1b00ee4825cb50c4059b771cf5b61ab Mon Sep 17 00:00:00 2001 From: lucas Date: Sat, 16 May 2026 20:01:50 -0300 Subject: [PATCH 15/16] Community order channel name should not be undefined, update model accordingly --- models/community.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/community.ts b/models/community.ts index cab69ae2..76b75fd8 100644 --- a/models/community.ts +++ b/models/community.ts @@ -17,7 +17,7 @@ export interface IOrderChannel extends Document { } const OrderChannelSchema = new Schema({ - name: { type: String, unique: true, sparse: true }, + name: { type: String, unique: true, trim: true }, type: { type: String, enum: ['buy', 'sell', 'mixed'], From 5a0c53f4ce1177ecbfe17d5c4a76af29ccfe34ae Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 18 May 2026 21:00:21 -0300 Subject: [PATCH 16/16] Fix fallback in getOrderChannel --- util/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/util/index.ts b/util/index.ts index d38cf34e..aeda27f7 100644 --- a/util/index.ts +++ b/util/index.ts @@ -319,7 +319,7 @@ const deleteOrderFromChannel = async (order: IOrder, telegram: Telegram) => { }; const getOrderChannel = async (order: IOrder, bot?: Telegram) => { - let channel = process.env.CHANNEL; + let channel; if (order.community_id) { const community = await Community.findOne({ _id: order.community_id }); if (!community) { @@ -351,6 +351,9 @@ const getOrderChannel = async (order: IOrder, bot?: Telegram) => { return undefined; } } + } else { + // no community order / order in the global community + channel = process.env.CHANNEL; } return channel;