Skip to content

Commit bf32684

Browse files
feat: add configurable user requirements for community creation (#841)
* feat: add configurable user requirements for community creation * fix: fail-closed on invalid COMMUNITY_CREATION_* env var values * fix: reject negative values for COMMUNITY_CREATION_* thresholds * harden the check on community creation configuration * Code formatting * Refactored threshold validation to helper functions, enforced fail-closed configuration checks, improved error logging levels, added explicit user date validation, and updated all locales to provide detailed feedback to the user on unmet requirements. * Improve community creation requirement gating by: - Renaming COMMUNITY_CREATION_MIN_VOLUME to COMMUNITY_CREATION_MIN_VOLUME_SATS and documenting the sats unit in .env-sample. - Extracting validation logic to a new module with high test coverage. - Implementing startup logging for misconfigurations. - Dynamically constructing requirement messages to show only active thresholds. - Splitting localization keys for granular control across all locales. --------- Co-authored-by: Maxi Vila <matovi98@gmail.com>
1 parent fdd5b2b commit bf32684

14 files changed

Lines changed: 342 additions & 0 deletions

File tree

.env-sample

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ NOSTR_SK=''
8686

8787
# Enable community creation via the /community command (default: false, opt-in per instance)
8888
COMMUNITY_CREATION_ENABLED=false
89+
# Minimum requirements to create a community (leave unset to disable the restriction)
90+
# Note: It is recommended to combine MIN_REPUTATION with MIN_ORDERS to avoid gaming the system.
91+
# MIN_VOLUME is measured in satoshis.
92+
# COMMUNITY_CREATION_MIN_ORDERS=100
93+
# COMMUNITY_CREATION_MIN_VOLUME_SATS=10000000
94+
# COMMUNITY_CREATION_MIN_DAYS_USING_BOT=365
95+
# COMMUNITY_CREATION_MIN_REPUTATION=4.8
8996

9097
# Number of currencies allowed in a community
9198
COMMUNITY_CURRENCIES=20

bot/modules/community/index.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Telegraf } from 'telegraf';
2+
import { logger } from '../../../logger';
23
import { userMiddleware, superAdminMiddleware } from '../../middleware/user';
34
import * as actions from './actions';
45
import * as commands from './commands';
@@ -9,12 +10,98 @@ import {
910
} from './messages';
1011
import { CommunityContext } from './communityContext';
1112
import * as Scenes from './scenes';
13+
import {
14+
getCommunityThresholds,
15+
meetsCommunityCreationRequirements,
16+
} from './requirements';
1217

1318
export const configure = (bot: Telegraf<CommunityContext>) => {
1419
bot.command('mycomm', userMiddleware, commands.communityAdmin);
1520
bot.command('mycomms', userMiddleware, commands.myComms);
1621
if (process.env.COMMUNITY_CREATION_ENABLED === 'true') {
22+
// Parsing of these values only happens once, they are not reparsed every time a user wants to create a new community
23+
const { invalidEnvVars, minOrders, minVolume, minDays, minReputation } =
24+
getCommunityThresholds();
25+
1726
bot.command('community', userMiddleware, async ctx => {
27+
const { user } = ctx;
28+
29+
if (invalidEnvVars.length > 0) {
30+
logger.error(
31+
`Community creation is failing due to a misconfiguration in initialization time: ${invalidEnvVars.join(', ')}`,
32+
);
33+
return ctx.reply(ctx.i18n.t('generic_error'));
34+
}
35+
36+
const { meets, daysUsing, hasInvalidDate } =
37+
meetsCommunityCreationRequirements(
38+
{
39+
tg_id: user.tg_id,
40+
trades_completed: user.trades_completed,
41+
volume_traded: user.volume_traded,
42+
total_rating: user.total_rating,
43+
created_at: user.created_at,
44+
},
45+
{
46+
minOrders,
47+
minVolume,
48+
minDays,
49+
minReputation,
50+
},
51+
);
52+
53+
if (hasInvalidDate) {
54+
logger.error(
55+
`User ${user.tg_id} has invalid created_at timestamp: ${user.created_at}`,
56+
);
57+
return ctx.reply(ctx.i18n.t('generic_error'));
58+
}
59+
60+
if (!meets) {
61+
logger.warning(
62+
`User ${user.tg_id} tried to create a community but does not meet the requirements - orders: ${user.trades_completed}, volume: ${user.volume_traded}, days: ${daysUsing}, reputation: ${user.total_rating}`,
63+
);
64+
65+
// Build the message dynamically based on active thresholds
66+
const lines = [
67+
ctx.i18n.t('community_creation_requirements_not_met_header'),
68+
];
69+
if (minOrders !== null) {
70+
lines.push(
71+
ctx.i18n.t('community_creation_requirements_not_met_orders', {
72+
userOrders: user.trades_completed,
73+
requiredOrders: minOrders,
74+
}),
75+
);
76+
}
77+
if (minVolume !== null) {
78+
lines.push(
79+
ctx.i18n.t('community_creation_requirements_not_met_volume', {
80+
userVolume: user.volume_traded,
81+
requiredVolume: minVolume,
82+
}),
83+
);
84+
}
85+
if (minDays !== null) {
86+
lines.push(
87+
ctx.i18n.t('community_creation_requirements_not_met_days', {
88+
userDays: daysUsing,
89+
requiredDays: minDays,
90+
}),
91+
);
92+
}
93+
if (minReputation !== null) {
94+
lines.push(
95+
ctx.i18n.t('community_creation_requirements_not_met_rep', {
96+
userRep: user.total_rating.toFixed(2),
97+
requiredRep: minReputation,
98+
}),
99+
);
100+
}
101+
102+
return ctx.reply(lines.join('\n'));
103+
}
104+
18105
await ctx.scene.enter('COMMUNITY_WIZARD_SCENE_ID', {
19106
bot,
20107
user: ctx.user,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { logger } from '../../../logger';
2+
3+
export interface ParsedThresholds {
4+
minOrders: number | null;
5+
minVolume: number | null;
6+
minDays: number | null;
7+
minReputation: number | null;
8+
invalidEnvVars: string[];
9+
}
10+
11+
export interface UserMetrics {
12+
tg_id: string;
13+
trades_completed: number;
14+
volume_traded: number;
15+
total_rating: number;
16+
created_at: Date | string;
17+
}
18+
19+
export const parseOptionalNumber = (
20+
raw: string | undefined,
21+
): { value: number | null; isValid: boolean } => {
22+
if (raw == null || raw.trim() === '') return { value: null, isValid: true };
23+
const n = Number(raw);
24+
if (Number.isFinite(n) && n >= 0) return { value: n, isValid: true };
25+
return { value: null, isValid: false };
26+
};
27+
28+
export const getCommunityThresholds = (): ParsedThresholds => {
29+
const thresholdResults = {
30+
COMMUNITY_CREATION_MIN_ORDERS: parseOptionalNumber(
31+
process.env.COMMUNITY_CREATION_MIN_ORDERS,
32+
),
33+
COMMUNITY_CREATION_MIN_VOLUME_SATS: parseOptionalNumber(
34+
process.env.COMMUNITY_CREATION_MIN_VOLUME_SATS,
35+
),
36+
COMMUNITY_CREATION_MIN_DAYS_USING_BOT: parseOptionalNumber(
37+
process.env.COMMUNITY_CREATION_MIN_DAYS_USING_BOT,
38+
),
39+
COMMUNITY_CREATION_MIN_REPUTATION: parseOptionalNumber(
40+
process.env.COMMUNITY_CREATION_MIN_REPUTATION,
41+
),
42+
};
43+
44+
const invalidEnvVars = Object.entries(thresholdResults)
45+
.filter(([_, res]) => !res.isValid)
46+
.map(([key]) => key);
47+
48+
if (invalidEnvVars.length > 0) {
49+
logger.error(
50+
`Invalid COMMUNITY_CREATION_* threshold configuration: ${invalidEnvVars.join(', ')}`,
51+
);
52+
}
53+
54+
return {
55+
minOrders: thresholdResults.COMMUNITY_CREATION_MIN_ORDERS.value,
56+
minVolume: thresholdResults.COMMUNITY_CREATION_MIN_VOLUME_SATS.value,
57+
minDays: thresholdResults.COMMUNITY_CREATION_MIN_DAYS_USING_BOT.value,
58+
minReputation: thresholdResults.COMMUNITY_CREATION_MIN_REPUTATION.value,
59+
invalidEnvVars,
60+
};
61+
};
62+
63+
export const meetsCommunityCreationRequirements = (
64+
user: UserMetrics,
65+
thresholds: Omit<ParsedThresholds, 'invalidEnvVars'>,
66+
): { meets: boolean; daysUsing: number | null; hasInvalidDate: boolean } => {
67+
const createdAtTime = new Date(user.created_at).getTime();
68+
if (!Number.isFinite(createdAtTime)) {
69+
return { meets: false, daysUsing: null, hasInvalidDate: true };
70+
}
71+
72+
const daysUsing = Math.floor((Date.now() - createdAtTime) / 86400000);
73+
const { minOrders, minVolume, minDays, minReputation } = thresholds;
74+
75+
const meets =
76+
(minOrders === null || user.trades_completed >= minOrders) &&
77+
(minVolume === null || user.volume_traded >= minVolume) &&
78+
(minDays === null || daysUsing >= minDays) &&
79+
(minReputation === null || user.total_rating >= minReputation);
80+
81+
return { meets, daysUsing, hasInvalidDate: false };
82+
};

locales/de.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,11 @@ payment_methods_reset: "Zahlungsmethoden entfernt. Benutzer können jetzt belieb
722722
payment_methods_wizard_commands: "/reset — alle Zahlungsmethoden entfernen und Standardverhalten wiederherstellen\n/exit — ohne Speichern beenden"
723723
community_disabled_by_admin: Ein Administrator hat deine Community ${communityName} deaktiviert
724724
community_enabled_by_admin: Ein Administrator hat deine Community ${communityName} wieder aktiviert
725+
community_creation_requirements_not_met_header: "Du erfüllst nicht die Anforderungen zur Erstellung einer Community:"
726+
community_creation_requirements_not_met_orders: "* Bestellungen: ${userOrders} / ${requiredOrders}"
727+
community_creation_requirements_not_met_volume: "* Volumen: ${userVolume} / ${requiredVolume}"
728+
community_creation_requirements_not_met_days: "* Tage der Nutzung des Bots: ${userDays} / ${requiredDays}"
729+
community_creation_requirements_not_met_rep: "* Reputation: ${userRep} / ${requiredRep}"
725730
community_already_disabled: Diese Community ist bereits deaktiviert
726731
community_already_enabled: Diese Community ist bereits aktiviert
727732
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
@@ -728,6 +728,11 @@ payment_methods_reset: "Payment methods removed. Users can now enter any payment
728728
payment_methods_wizard_commands: "/reset — remove all payment methods and restore default behavior\n/exit — exit without saving"
729729
community_disabled_by_admin: An administrator has disabled your community ${communityName}
730730
community_enabled_by_admin: An administrator has re-enabled your community ${communityName}
731+
community_creation_requirements_not_met_header: "You do not meet the requirements to create a community:"
732+
community_creation_requirements_not_met_orders: "* Orders: ${userOrders} / ${requiredOrders}"
733+
community_creation_requirements_not_met_volume: "* Volume: ${userVolume} / ${requiredVolume}"
734+
community_creation_requirements_not_met_days: "* Days using the bot: ${userDays} / ${requiredDays}"
735+
community_creation_requirements_not_met_rep: "* Reputation: ${userRep} / ${requiredRep}"
731736
community_already_disabled: This community is already disabled
732737
community_already_enabled: This community is already enabled
733738
ambiguous_community_input: Multiple communities match that username, please use the community ID instead

locales/es.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,11 @@ payment_methods_reset: "Métodos de pago eliminados. Los usuarios ahora pueden i
725725
payment_methods_wizard_commands: "/reset — eliminar todos los métodos de pago y restaurar el comportamiento predeterminado\n/exit — salir sin guardar"
726726
community_disabled_by_admin: Un administrador ha deshabilitado tu comunidad ${communityName}
727727
community_enabled_by_admin: Un administrador ha reactivado tu comunidad ${communityName}
728+
community_creation_requirements_not_met_header: "No cumples con los requisitos para crear una comunidad:"
729+
community_creation_requirements_not_met_orders: "* Órdenes: ${userOrders} / ${requiredOrders}"
730+
community_creation_requirements_not_met_volume: "* Volumen: ${userVolume} / ${requiredVolume}"
731+
community_creation_requirements_not_met_days: "* Días usando el bot: ${userDays} / ${requiredDays}"
732+
community_creation_requirements_not_met_rep: "* Reputación: ${userRep} / ${requiredRep}"
728733
community_already_disabled: Esta comunidad ya está deshabilitada
729734
community_already_enabled: Esta comunidad ya está habilitada
730735
ambiguous_community_input: Varias comunidades coinciden con ese nombre de usuario, por favor usa el ID de la comunidad

locales/fa.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,11 @@ payment_methods_reset: "روش‌های پرداخت حذف شدند. کاربر
821821
payment_methods_wizard_commands: "/reset — حذف همه روش‌های پرداخت و بازگرداندن رفتار پیش‌فرض\n/exit — خروج بدون ذخیره"
822822
community_disabled_by_admin: یک مدیر جامعه ${communityName} شما را غیرفعال کرد
823823
community_enabled_by_admin: یک مدیر جامعه ${communityName} شما را دوباره فعال کرد
824+
community_creation_requirements_not_met_header: "شما شرایط لازم برای ایجاد جامعه را ندارید:"
825+
community_creation_requirements_not_met_orders: "* سفارش‌ها: ${userOrders} / ${requiredOrders}"
826+
community_creation_requirements_not_met_volume: "* حجم: ${userVolume} / ${requiredVolume}"
827+
community_creation_requirements_not_met_days: "* روزهای استفاده از ربات: ${userDays} / ${requiredDays}"
828+
community_creation_requirements_not_met_rep: "* اعتبار: ${userRep} / ${requiredRep}"
824829
community_already_disabled: این جامعه از قبل غیرفعال است
825830
community_already_enabled: این جامعه از قبل فعال است
826831
ambiguous_community_input: چندین جامعه با این نام کاربری مطابقت دارند، لطفاً از شناسه جامعه استفاده کنید

locales/fr.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,11 @@ payment_methods_reset: "Méthodes de paiement supprimées. Les utilisateurs peuv
721721
payment_methods_wizard_commands: "/reset — supprimer toutes les méthodes de paiement et restaurer le comportement par défaut\n/exit — quitter sans enregistrer"
722722
community_disabled_by_admin: Un administrateur a désactivé votre communauté ${communityName}
723723
community_enabled_by_admin: Un administrateur a réactivé votre communauté ${communityName}
724+
community_creation_requirements_not_met_header: "Vous ne remplissez pas les conditions pour créer une communauté :"
725+
community_creation_requirements_not_met_orders: "* Commandes : ${userOrders} / ${requiredOrders}"
726+
community_creation_requirements_not_met_volume: "* Volume : ${userVolume} / ${requiredVolume}"
727+
community_creation_requirements_not_met_days: "* Jours d'utilisation du bot : ${userDays} / ${requiredDays}"
728+
community_creation_requirements_not_met_rep: "* Réputation : ${userRep} / ${requiredRep}"
724729
community_already_disabled: Cette communauté est déjà désactivée
725730
community_already_enabled: Cette communauté est déjà activée
726731
ambiguous_community_input: Plusieurs communautés correspondent à ce nom d'utilisateur, veuillez utiliser l'ID de la communauté à la place

locales/it.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,11 @@ payment_methods_reset: "Metodi di pagamento rimossi. Gli utenti possono ora inse
719719
payment_methods_wizard_commands: "/reset — rimuovere tutti i metodi di pagamento e ripristinare il comportamento predefinito\n/exit — uscire senza salvare"
720720
community_disabled_by_admin: Un amministratore ha disabilitato la tua comunità ${communityName}
721721
community_enabled_by_admin: Un amministratore ha riabilitato la tua comunità ${communityName}
722+
community_creation_requirements_not_met_header: "Non soddisfi i requisiti per creare una comunità:"
723+
community_creation_requirements_not_met_orders: "* Ordini: ${userOrders} / ${requiredOrders}"
724+
community_creation_requirements_not_met_volume: "* Volume: ${userVolume} / ${requiredVolume}"
725+
community_creation_requirements_not_met_days: "* Giorni di utilizzo del bot: ${userDays} / ${requiredDays}"
726+
community_creation_requirements_not_met_rep: "* Reputazione: ${userRep} / ${requiredRep}"
722727
community_already_disabled: Questa comunità è già disabilitata
723728
community_already_enabled: Questa comunità è già abilitata
724729
ambiguous_community_input: Più comunità corrispondono a quel nome utente, usa invece l'ID della comunità

locales/ko.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,11 @@ payment_methods_reset: "결제 방법이 삭제되었습니다. 이제 사용자
719719
payment_methods_wizard_commands: "/reset — 모든 결제 방법을 삭제하고 기본 동작을 복원합니다\n/exit — 저장하지 않고 종료"
720720
community_disabled_by_admin: 관리자가 커뮤니티 ${communityName}을(를) 비활성화했습니다
721721
community_enabled_by_admin: 관리자가 커뮤니티 ${communityName}을(를) 다시 활성화했습니다
722+
community_creation_requirements_not_met_header: "커뮤니티를 생성하기 위한 요건을 충족하지 못했습니다:"
723+
community_creation_requirements_not_met_orders: "* 주문: ${userOrders} / ${requiredOrders}"
724+
community_creation_requirements_not_met_volume: "* 거래량: ${userVolume} / ${requiredVolume}"
725+
community_creation_requirements_not_met_days: "* 봇 사용 일수: ${userDays} / ${requiredDays}"
726+
community_creation_requirements_not_met_rep: "* 평판: ${userRep} / ${requiredRep}"
722727
community_already_disabled: 이 커뮤니티는 이미 비활성화되어 있습니다
723728
community_already_enabled: 이 커뮤니티는 이미 활성화되어 있습니다
724729
ambiguous_community_input: 해당 사용자 이름과 일치하는 커뮤니티가 여러 개 있습니다. 커뮤니티 ID를 사용해 주세요

0 commit comments

Comments
 (0)