Skip to content

Commit 2a2beea

Browse files
authored
feat: add disablecommunity/enablecommunity superadmin commands (#817)
* feat: add disablecommunity/enablecommunity admin commands with soft-disable via enabled field * i18n: translate disable/enable community admin messages to all supported languages * fix: reply with community_not_found instead of throwing on disabled/missing community * fix: preserve default_community_id on disabled community and revalidate before setCommunity * revert: restore default_community_id cleanup on disabled community lookup * fix: handle disabled community gracefully in enterWizard, settings and validateAdmin * i18n: add ambiguous_community_input translation key to all locales
1 parent fefc905 commit 2a2beea

20 files changed

Lines changed: 235 additions & 22 deletions

File tree

bot/modules/community/actions.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ const getVolumeNDays = async (
5959

6060
export const onCommunityInfo = async (ctx: MainContext) => {
6161
const commId = ctx.match?.[1];
62-
const community = await Community.findById(commId);
63-
if (community === null) throw new Error('community not found');
62+
const community = await Community.findOne({
63+
_id: commId,
64+
enabled: { $ne: false },
65+
});
66+
if (community === null) return ctx.reply(ctx.i18n.t('community_not_found'));
6467
const userCount = await User.countDocuments({ default_community_id: commId });
6568
const orderCount = await getOrdersNDays(1, commId);
6669
const volume = await getVolumeNDays(1, commId);
@@ -111,6 +114,11 @@ export const onCommunityInfo = async (ctx: MainContext) => {
111114
export const onSetCommunity = async (ctx: CommunityContext) => {
112115
const tgId = (ctx.update as any).callback_query.from.id;
113116
const defaultCommunityId = ctx.match?.[1];
117+
const community = await Community.findOne({
118+
_id: defaultCommunityId,
119+
enabled: { $ne: false },
120+
});
121+
if (!community) return ctx.reply(ctx.i18n.t('community_not_found'));
114122
await User.findOneAndUpdate(
115123
{ tg_id: tgId },
116124
{ default_community_id: defaultCommunityId },
@@ -120,7 +128,11 @@ export const onSetCommunity = async (ctx: CommunityContext) => {
120128

121129
export const withdrawEarnings = async (ctx: CommunityContext) => {
122130
ctx.deleteMessage();
123-
const community = await Community.findById(ctx.match?.[1]);
131+
const community = await Community.findOne({
132+
_id: ctx.match?.[1],
133+
enabled: { $ne: false },
134+
});
135+
if (!community) return ctx.reply(ctx.i18n.t('community_not_found'));
124136
ctx.scene.enter('ADD_EARNINGS_INVOICE_WIZARD_SCENE_ID', {
125137
community,
126138
});

bot/modules/community/commands.ts

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* eslint-disable no-underscore-dangle */
22
import { logger } from '../../../logger';
33
import { showUserCommunitiesMessage } from './messages';
4-
import { Community, Order } from '../../../models';
4+
import { Community, Order, User } from '../../../models';
55
import { validateParams, validateObjectId } from '../../validations';
66
import { MainContext } from '../../start';
77
import { CommunityContext } from './communityContext';
88
import { Telegraf } from 'telegraf';
9+
import { getUserI18nContext } from '../../../util';
910

1011
async function getOrderCountByCommunity(): Promise<number[]> {
1112
const data = await Order.aggregate([
@@ -21,6 +22,7 @@ async function findCommunities(currency: string) {
2122
const communities = await Community.find({
2223
currencies: currency,
2324
public: true,
25+
enabled: { $ne: false },
2426
});
2527
const orderCount = await getOrderCountByCommunity();
2628
return communities.map(comm => {
@@ -49,9 +51,15 @@ export const setComm = async (ctx: MainContext) => {
4951
if (groupName[0] == '@') {
5052
// Allow find communities case insensitive
5153
const regex = new RegExp(['^', groupName, '$'].join(''), 'i');
52-
community = await Community.findOne({ group: regex });
54+
community = await Community.findOne({
55+
group: regex,
56+
enabled: { $ne: false },
57+
});
5358
} else if (groupName[0] == '-') {
54-
community = await Community.findOne({ group: groupName });
59+
community = await Community.findOne({
60+
group: groupName,
61+
enabled: { $ne: false },
62+
});
5563
}
5664
if (!community) {
5765
return await ctx.reply(ctx.i18n.t('community_not_found'));
@@ -70,7 +78,11 @@ export const communityAdmin = async (ctx: CommunityContext) => {
7078
try {
7179
const [group] = (await validateParams(ctx, 2, '\\<_community_\\>'))!;
7280
const creator_id = ctx.user.id;
73-
const [community] = await Community.find({ group, creator_id });
81+
const [community] = await Community.find({
82+
group,
83+
creator_id,
84+
enabled: { $ne: false },
85+
});
7486
if (!community) throw new Error('CommunityNotFound');
7587
await ctx.scene.enter('COMMUNITY_ADMIN', { community });
7688
} catch (err: any) {
@@ -89,7 +101,10 @@ export const myComms = async (ctx: MainContext) => {
89101
try {
90102
const { user } = ctx;
91103

92-
const communities = await Community.find({ creator_id: user._id });
104+
const communities = await Community.find({
105+
creator_id: user._id,
106+
enabled: { $ne: false },
107+
});
93108

94109
if (!communities.length)
95110
return await ctx.reply(ctx.i18n.t('you_dont_have_communities'));
@@ -147,6 +162,7 @@ export const updateCommunity = async (
147162
const community = await Community.findOne({
148163
_id: id,
149164
creator_id: user._id,
165+
enabled: { $ne: false },
150166
});
151167

152168
if (!community) {
@@ -228,6 +244,7 @@ export const deleteCommunity = async (ctx: CommunityContext) => {
228244
const community = await Community.findOne({
229245
_id: id,
230246
creator_id: ctx.user._id,
247+
enabled: { $ne: false },
231248
});
232249

233250
if (!community) {
@@ -241,6 +258,105 @@ export const deleteCommunity = async (ctx: CommunityContext) => {
241258
}
242259
};
243260

261+
async function findCommunityByInput(
262+
ctx: MainContext,
263+
input: string,
264+
): Promise<typeof Community.prototype | null> {
265+
if (input[0] === '@') {
266+
const regex = new RegExp(
267+
`^${input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
268+
'i',
269+
);
270+
const matches = await Community.find({ group: regex }).limit(2);
271+
if (matches.length > 1) throw new Error('AmbiguousCommunityInput');
272+
return matches[0] ?? null;
273+
}
274+
if (!(await validateObjectId(ctx, input))) return null;
275+
return Community.findOne({ _id: input });
276+
}
277+
278+
export const disableCommunity = async (ctx: MainContext) => {
279+
try {
280+
const [input] = (await validateParams(
281+
ctx,
282+
2,
283+
'\\<_community id \\| @groupUsername_\\>',
284+
))!;
285+
if (!input) return;
286+
287+
const community = await findCommunityByInput(ctx, input);
288+
if (!community) return ctx.reply(ctx.i18n.t('community_not_found'));
289+
if (community.enabled === false)
290+
return ctx.reply(ctx.i18n.t('community_already_disabled'));
291+
292+
community.enabled = false;
293+
await community.save();
294+
295+
const creator = await User.findById(community.creator_id);
296+
if (creator) {
297+
try {
298+
const creatorI18n = await getUserI18nContext(creator);
299+
await ctx.telegram.sendMessage(
300+
creator.tg_id,
301+
creatorI18n.t('community_disabled_by_admin', {
302+
communityName: community.name,
303+
}),
304+
);
305+
} catch (notifyError) {
306+
logger.error(notifyError);
307+
}
308+
}
309+
310+
return ctx.reply(ctx.i18n.t('operation_successful'));
311+
} catch (error: any) {
312+
if (error.message === 'AmbiguousCommunityInput')
313+
return ctx.reply(ctx.i18n.t('ambiguous_community_input'));
314+
logger.error(error);
315+
await ctx.reply(ctx.i18n.t('generic_error'));
316+
}
317+
};
318+
319+
export const enableCommunity = async (ctx: MainContext) => {
320+
try {
321+
const [input] = (await validateParams(
322+
ctx,
323+
2,
324+
'\\<_community id \\| @groupUsername_\\>',
325+
))!;
326+
if (!input) return;
327+
328+
const community = await findCommunityByInput(ctx, input);
329+
if (!community) return ctx.reply(ctx.i18n.t('community_not_found'));
330+
if (community.enabled !== false)
331+
return ctx.reply(ctx.i18n.t('community_already_enabled'));
332+
333+
community.enabled = true;
334+
await community.save();
335+
336+
const creator = await User.findById(community.creator_id);
337+
if (creator) {
338+
try {
339+
const creatorI18n = await getUserI18nContext(creator);
340+
await ctx.telegram.sendMessage(
341+
creator.tg_id,
342+
creatorI18n.t('community_enabled_by_admin', {
343+
communityName: community.name,
344+
}),
345+
);
346+
} catch (notifyError) {
347+
logger.error(notifyError);
348+
}
349+
}
350+
351+
return ctx.reply(ctx.i18n.t('operation_successful'));
352+
} catch (error: any) {
353+
if (error.message === 'AmbiguousCommunityInput')
354+
return ctx.reply(ctx.i18n.t('ambiguous_community_input'));
355+
logger.error(error);
356+
await ctx.reply(ctx.i18n.t('generic_error'));
357+
}
358+
};
359+
244360
export const changeVisibility = async (ctx: CommunityContext) => {
245361
try {
246362
ctx.deleteMessage();
@@ -251,6 +367,7 @@ export const changeVisibility = async (ctx: CommunityContext) => {
251367
const community = await Community.findOne({
252368
_id: id,
253369
creator_id: ctx.user._id,
370+
enabled: { $ne: false },
254371
});
255372

256373
if (!community) {

bot/modules/community/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Telegraf } from 'telegraf';
2-
import { userMiddleware } from '../../middleware/user';
2+
import { userMiddleware, superAdminMiddleware } from '../../middleware/user';
33
import * as actions from './actions';
44
import * as commands from './commands';
55
import {
@@ -68,6 +68,17 @@ export const configure = (bot: Telegraf<CommunityContext>) => {
6868
},
6969
);
7070

71+
bot.command(
72+
'disablecommunity',
73+
superAdminMiddleware,
74+
commands.disableCommunity,
75+
);
76+
bot.command(
77+
'enablecommunity',
78+
superAdminMiddleware,
79+
commands.enableCommunity,
80+
);
81+
7182
bot.command('findcomms', userMiddleware, commands.findCommunity);
7283
bot.action(
7384
/^communityInfo_([0-9a-f]{24})$/,

bot/modules/community/messages.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,12 @@ export const updateCommunityMessage = async (ctx: MainContext) => {
5050
try {
5151
await ctx.deleteMessage();
5252
const id = ctx.match?.[1];
53-
const community = await Community.findById(id);
54-
if (community == null) throw new Error('community was not found');
53+
const community = await Community.findOne({
54+
_id: id,
55+
enabled: { $ne: false },
56+
});
57+
if (community == null)
58+
return await ctx.reply(ctx.i18n.t('community_not_found'));
5559
let text = ctx.i18n.t('community') + `: ${community.name}\n`;
5660
text += ctx.i18n.t('what_to_do');
5761
const visibilityText = community.public
@@ -170,8 +174,12 @@ export const earningsMessage = async (ctx: MainContext) => {
170174
if (isScheduled)
171175
return await ctx.reply(ctx.i18n.t('invoice_already_being_paid'));
172176

173-
const community = await Community.findById(communityId);
174-
if (community == null) throw new Error('community was not found');
177+
const community = await Community.findOne({
178+
_id: communityId,
179+
enabled: { $ne: false },
180+
});
181+
if (community == null)
182+
return await ctx.reply(ctx.i18n.t('community_not_found'));
175183
const button =
176184
community.earnings > 0
177185
? {

bot/modules/orders/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ async function enterWizard(
197197
const { community, isBanned } = communityInfo;
198198

199199
if (!community) {
200-
throw new Error('Default community not found');
200+
return deletedCommunityMessage(ctx);
201201
}
202202

203203
// Check if user is banned

bot/modules/orders/scenes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ const createOrderSteps = {
154154
const stateComm = ctx.wizard.state.community;
155155
const loadedComm =
156156
!stateComm && user?.default_community_id
157-
? await Community.findById(user.default_community_id)
157+
? await Community.findOne({
158+
_id: user.default_community_id,
159+
enabled: { $ne: false },
160+
})
158161
: null;
159162
const community = stateComm ?? loadedComm;
160163
if (loadedComm) ctx.wizard.state.community = loadedComm;

bot/modules/user/scenes/settings.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ function make() {
2626
lightning_address: '',
2727
};
2828
if (user.default_community_id) {
29-
const community = await Community.findById(user.default_community_id);
30-
if (community == null) throw new Error('community not found');
31-
data.community = community.group;
29+
const community = await Community.findOne({
30+
_id: user.default_community_id,
31+
enabled: { $ne: false },
32+
});
33+
if (community) data.community = community.group;
3234
}
3335
if (user.nostr_public_key) {
3436
data.npub = NostrLib.encodeNpub(user.nostr_public_key);

bot/validations.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ const validateAdmin = async (ctx: MainContext, id?: string) => {
111111

112112
let community = null;
113113
if (user.default_community_id)
114-
community = await Community.findOne({ _id: user.default_community_id });
114+
community = await Community.findOne({
115+
_id: user.default_community_id,
116+
enabled: { $ne: false },
117+
});
115118

116119
const isSolver = isDisputeSolver(community, user);
117120

locales/de.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,8 @@ payment_methods_saved: "Zahlungsmethoden gespeichert ✅"
719719
custom_payment_method: "✍️ Benutzerdefinierte Zahlungsmethode"
720720
payment_methods_reset: "Zahlungsmethoden entfernt. Benutzer können jetzt beliebige Zahlungsmethoden frei eingeben."
721721
payment_methods_wizard_commands: "/reset — alle Zahlungsmethoden entfernen und Standardverhalten wiederherstellen\n/exit — ohne Speichern beenden"
722+
community_disabled_by_admin: Ein Administrator hat deine Community ${communityName} deaktiviert
723+
community_enabled_by_admin: Ein Administrator hat deine Community ${communityName} wieder aktiviert
724+
community_already_disabled: Diese Community ist bereits deaktiviert
725+
community_already_enabled: Diese Community ist bereits aktiviert
726+
ambiguous_community_input: Mehrere Communities stimmen mit diesem Benutzernamen überein, bitte verwende stattdessen die Community-ID

locales/en.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,3 +724,8 @@ payment_methods_saved: "Payment methods saved ✅"
724724
custom_payment_method: "✍️ Custom payment method"
725725
payment_methods_reset: "Payment methods removed. Users can now enter any payment method freely."
726726
payment_methods_wizard_commands: "/reset — remove all payment methods and restore default behavior\n/exit — exit without saving"
727+
community_disabled_by_admin: An administrator has disabled your community ${communityName}
728+
community_enabled_by_admin: An administrator has re-enabled your community ${communityName}
729+
community_already_disabled: This community is already disabled
730+
community_already_enabled: This community is already enabled
731+
ambiguous_community_input: Multiple communities match that username, please use the community ID instead

0 commit comments

Comments
 (0)