Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ MONITOR_URL=''
MONITOR_AUTH_TOKEN=''
# Bot name identifier sent with heartbeats (default: lnp2pBot)
MONITOR_BOT_NAME='lnp2pBot'

# Set to 'true' to enable the /community command, allowing users to create new communities.
# Default is false (disabled) so bot operators can run closed instances without exposing community creation.
COMMUNITY_CREATION_ENABLED=false
13 changes: 11 additions & 2 deletions bot/modules/community/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ const getVolumeNDays = async (

export const onCommunityInfo = async (ctx: MainContext) => {
const commId = ctx.match?.[1];
const community = await Community.findById(commId);
const community = await Community.findOne({
_id: commId,
enabled: { $ne: false },
});
if (community === null) throw new Error('community not found');
const userCount = await User.countDocuments({ default_community_id: commId });
const orderCount = await getOrdersNDays(1, commId);
Expand Down Expand Up @@ -120,7 +123,13 @@ export const onSetCommunity = async (ctx: CommunityContext) => {

export const withdrawEarnings = async (ctx: CommunityContext) => {
ctx.deleteMessage();
const community = await Community.findById(ctx.match?.[1]);
const community = await Community.findOne({
_id: ctx.match?.[1],
enabled: { $ne: false },
});
if (community == null) {
return ctx.reply(ctx.i18n.t('community_not_found'));
}
ctx.scene.enter('ADD_EARNINGS_INVOICE_WIZARD_SCENE_ID', {
community,
});
Expand Down
165 changes: 160 additions & 5 deletions bot/modules/community/commands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle */
import { logger } from '../../../logger';
import { showUserCommunitiesMessage } from './messages';
import { Community, Order } from '../../../models';
import { Community, Order, User } from '../../../models';
import { validateParams, validateObjectId } from '../../validations';
import { MainContext } from '../../start';
import { CommunityContext } from './communityContext';
Expand All @@ -21,6 +21,7 @@ async function findCommunities(currency: string) {
const communities = await Community.find({
currencies: currency,
public: true,
enabled: { $ne: false },
});
const orderCount = await getOrderCountByCommunity();
return communities.map(comm => {
Expand Down Expand Up @@ -49,9 +50,15 @@ export const setComm = async (ctx: MainContext) => {
if (groupName[0] == '@') {
// Allow find communities case insensitive
const regex = new RegExp(['^', groupName, '$'].join(''), 'i');
community = await Community.findOne({ group: regex });
community = await Community.findOne({
group: regex,
enabled: { $ne: false },
});
} else if (groupName[0] == '-') {
community = await Community.findOne({ group: groupName });
community = await Community.findOne({
group: groupName,
enabled: { $ne: false },
});
}
if (!community) {
return await ctx.reply(ctx.i18n.t('community_not_found'));
Expand All @@ -70,7 +77,11 @@ export const communityAdmin = async (ctx: CommunityContext) => {
try {
const [group] = (await validateParams(ctx, 2, '\\<_community_\\>'))!;
const creator_id = ctx.user.id;
const [community] = await Community.find({ group, creator_id });
const [community] = await Community.find({
group,
creator_id,
enabled: { $ne: false },
});
if (!community) throw new Error('CommunityNotFound');
await ctx.scene.enter('COMMUNITY_ADMIN', { community });
} catch (err: any) {
Expand All @@ -89,7 +100,10 @@ export const myComms = async (ctx: MainContext) => {
try {
const { user } = ctx;

const communities = await Community.find({ creator_id: user._id });
const communities = await Community.find({
creator_id: user._id,
enabled: { $ne: false },
});

if (!communities.length)
return await ctx.reply(ctx.i18n.t('you_dont_have_communities'));
Expand Down Expand Up @@ -147,6 +161,7 @@ export const updateCommunity = async (
const community = await Community.findOne({
_id: id,
creator_id: user._id,
enabled: { $ne: false },
});

if (!community) {
Expand Down Expand Up @@ -228,6 +243,7 @@ export const deleteCommunity = async (ctx: CommunityContext) => {
const community = await Community.findOne({
_id: id,
creator_id: ctx.user._id,
enabled: { $ne: false },
});

if (!community) {
Expand All @@ -241,6 +257,144 @@ export const deleteCommunity = async (ctx: CommunityContext) => {
}
};

async function findCommunityByInput(
ctx: MainContext,
input: string,
): Promise<typeof Community.prototype | null | undefined> {
if (input[0] === '@') {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`^${escapedInput}$`, 'i');
return Community.findOne({ group: regex });
}
if (!(await validateObjectId(ctx, input))) return undefined;
return Community.findOne({ _id: input });
}

function buildCommunityInfoText(
ctx: MainContext,
community: typeof Community.prototype,
creatorUsername: string,
localeKey: string,
): string {
const solversText =
community.solvers.length > 0
? community.solvers
.map((s: { username: string }) => `@${s.username}`)
.join(', ')
: '-';
const groupText = community.group || '-';
return ctx.i18n.t(localeKey, {
communityName: community.name,
group: groupText,
solvers: solversText,
creatorUsername,
});
}

export const disableCommunity = async (ctx: MainContext) => {
try {
const [input] = (await validateParams(
ctx,
2,
'\\<_community id \\| @groupUsername_\\>',
))!;
if (!input) return;

const community = await findCommunityByInput(ctx, input);
if (community === undefined) return;
if (community === null) {
return ctx.reply(ctx.i18n.t('community_not_found'));
}

if (community.enabled === false) {
return ctx.reply(ctx.i18n.t('community_already_disabled'));
}

community.enabled = false;
await community.save();

const creator = await User.findById(community.creator_id);
const creatorUsername = creator?.username || 'unknown';

if (creator) {
try {
await ctx.telegram.sendMessage(
creator.tg_id,
ctx.i18n.t('community_disabled_by_admin', {
communityName: community.name,
}),
);
} catch (notifyError) {
logger.error(notifyError);
}
}

return ctx.reply(
buildCommunityInfoText(
ctx,
community,
creatorUsername,
'community_disabled_info',
),
);
} catch (error) {
logger.error(error);
await ctx.reply(ctx.i18n.t('generic_error'));
}
};

export const enableCommunity = async (ctx: MainContext) => {
try {
const [input] = (await validateParams(
ctx,
2,
'\\<_community id \\| @groupUsername_\\>',
))!;
if (!input) return;

const community = await findCommunityByInput(ctx, input);
if (community === undefined) return;
if (community === null) {
return ctx.reply(ctx.i18n.t('community_not_found'));
}

if (community.enabled !== false) {
return ctx.reply(ctx.i18n.t('community_already_enabled'));
}

community.enabled = true;
await community.save();

const creator = await User.findById(community.creator_id);
const creatorUsername = creator?.username || 'unknown';

if (creator) {
try {
await ctx.telegram.sendMessage(
creator.tg_id,
ctx.i18n.t('community_enabled_by_admin', {
communityName: community.name,
}),
);
} catch (notifyError) {
logger.error(notifyError);
}
}

return ctx.reply(
buildCommunityInfoText(
ctx,
community,
creatorUsername,
'community_enabled_info',
),
);
} catch (error) {
logger.error(error);
await ctx.reply(ctx.i18n.t('generic_error'));
}
};

export const changeVisibility = async (ctx: CommunityContext) => {
try {
ctx.deleteMessage();
Expand All @@ -251,6 +405,7 @@ export const changeVisibility = async (ctx: CommunityContext) => {
const community = await Community.findOne({
_id: id,
creator_id: ctx.user._id,
enabled: { $ne: false },
});

if (!community) {
Expand Down
26 changes: 20 additions & 6 deletions bot/modules/community/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Telegraf } from 'telegraf';
import { userMiddleware } from '../../middleware/user';
import { userMiddleware, superAdminMiddleware } from '../../middleware/user';
import * as actions from './actions';
import * as commands from './commands';
import {
Expand All @@ -13,11 +13,14 @@ import * as Scenes from './scenes';
export const configure = (bot: Telegraf<CommunityContext>) => {
bot.command('mycomm', userMiddleware, commands.communityAdmin);
bot.command('mycomms', userMiddleware, commands.myComms);
// TODO: Uncomment when the community wizard is ready
// bot.command('community', userMiddleware, async ctx => {
// const { user } = ctx;
// await ctx.scene.enter('COMMUNITY_WIZARD_SCENE_ID', { bot, user });
// });
if (process.env.COMMUNITY_CREATION_ENABLED === 'true') {
bot.command('community', userMiddleware, async ctx => {
await ctx.scene.enter('COMMUNITY_WIZARD_SCENE_ID', {
bot,
user: ctx.user,
});
});
}
bot.command('setcomm', userMiddleware, commands.setComm);

bot.action(
Expand Down Expand Up @@ -65,6 +68,17 @@ export const configure = (bot: Telegraf<CommunityContext>) => {
},
);

bot.command(
'disablecommunity',
superAdminMiddleware,
commands.disableCommunity,
);
bot.command(
'enablecommunity',
superAdminMiddleware,
commands.enableCommunity,
);

bot.command('findcomms', userMiddleware, commands.findCommunity);
bot.action(
/^communityInfo_([0-9a-f]{24})$/,
Expand Down
10 changes: 8 additions & 2 deletions bot/modules/community/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export const updateCommunityMessage = async (ctx: MainContext) => {
try {
await ctx.deleteMessage();
const id = ctx.match?.[1];
const community = await Community.findById(id);
const community = await Community.findOne({
_id: id,
enabled: { $ne: false },
});
if (community == null) throw new Error('community was not found');
let text = ctx.i18n.t('community') + `: ${community.name}\n`;
text += ctx.i18n.t('what_to_do');
Expand Down Expand Up @@ -170,7 +173,10 @@ export const earningsMessage = async (ctx: MainContext) => {
if (isScheduled)
return await ctx.reply(ctx.i18n.t('invoice_already_being_paid'));

const community = await Community.findById(communityId);
const community = await Community.findOne({
_id: communityId,
enabled: { $ne: false },
});
if (community == null) throw new Error('community was not found');
const button =
community.earnings > 0
Expand Down
5 changes: 4 additions & 1 deletion bot/modules/orders/scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ const createOrderSteps = {
const stateComm = ctx.wizard.state.community;
const loadedComm =
!stateComm && user?.default_community_id
? await Community.findById(user.default_community_id)
? await Community.findOne({
_id: user.default_community_id,
enabled: { $ne: false },
})
: null;
const community = stateComm ?? loadedComm;
if (loadedComm) ctx.wizard.state.community = loadedComm;
Expand Down
10 changes: 7 additions & 3 deletions bot/modules/user/scenes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ function make() {
lightning_address: '',
};
if (user.default_community_id) {
const community = await Community.findById(user.default_community_id);
if (community == null) throw new Error('community not found');
data.community = community.group;
const community = await Community.findOne({
_id: user.default_community_id,
enabled: { $ne: false },
});
if (community != null) {
data.community = community.group;
}
}
if (user.nostr_public_key) {
data.npub = NostrLib.encodeNpub(user.nostr_public_key);
Expand Down
6 changes: 6 additions & 0 deletions locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,9 @@ payment_methods_saved: "Zahlungsmethoden gespeichert ✅"
custom_payment_method: "✍️ Benutzerdefinierte Zahlungsmethode"
payment_methods_reset: "Zahlungsmethoden entfernt. Benutzer können jetzt beliebige Zahlungsmethoden frei eingeben."
payment_methods_wizard_commands: "/reset — alle Zahlungsmethoden entfernen und Standardverhalten wiederherstellen\n/exit — ohne Speichern beenden"
community_disabled_info: "Gemeinschaft: ${communityName}\nGruppe: ${group}\nLöser: ${solvers}\nErsteller: @${creatorUsername}\nDie Gemeinschaft wurde deaktiviert"
community_enabled_info: "Gemeinschaft: ${communityName}\nGruppe: ${group}\nLöser: ${solvers}\nErsteller: @${creatorUsername}\nDie Gemeinschaft wurde aktiviert"
community_disabled_by_admin: Ein Administrator hat Ihre Gemeinschaft ${communityName} deaktiviert
community_enabled_by_admin: Ein Administrator hat Ihre Gemeinschaft ${communityName} wieder aktiviert
community_already_disabled: Diese Gemeinschaft ist bereits deaktiviert
community_already_enabled: Diese Gemeinschaft ist bereits aktiv
6 changes: 6 additions & 0 deletions locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,9 @@ payment_methods_saved: "Payment methods saved ✅"
custom_payment_method: "✍️ Custom payment method"
payment_methods_reset: "Payment methods removed. Users can now enter any payment method freely."
payment_methods_wizard_commands: "/reset — remove all payment methods and restore default behavior\n/exit — exit without saving"
community_disabled_info: "Community: ${communityName}\nGroup: ${group}\nSolvers: ${solvers}\nCreator: @${creatorUsername}\nThe community was disabled"
community_enabled_info: "Community: ${communityName}\nGroup: ${group}\nSolvers: ${solvers}\nCreator: @${creatorUsername}\nThe community was enabled"
community_disabled_by_admin: An administrator has disabled your community ${communityName}
community_enabled_by_admin: An administrator has re-enabled your community ${communityName}
community_already_disabled: This community is already disabled
community_already_enabled: This community is already enabled
6 changes: 6 additions & 0 deletions locales/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,9 @@ payment_methods_saved: "Métodos de pago guardados ✅"
custom_payment_method: "✍️ Método de pago personalizado"
payment_methods_reset: "Métodos de pago eliminados. Los usuarios ahora pueden ingresar cualquier método de pago libremente."
payment_methods_wizard_commands: "/reset — eliminar todos los métodos de pago y restaurar el comportamiento predeterminado\n/exit — salir sin guardar"
community_disabled_info: "Comunidad: ${communityName}\nGrupo: ${group}\nSolvers: ${solvers}\nCreador: @${creatorUsername}\nLa comunidad fue deshabilitada"
community_enabled_info: "Comunidad: ${communityName}\nGrupo: ${group}\nSolvers: ${solvers}\nCreador: @${creatorUsername}\nLa comunidad fue habilitada"
community_disabled_by_admin: Un administrador ha deshabilitado tu comunidad ${communityName}
community_enabled_by_admin: Un administrador ha reactivado tu comunidad ${communityName}
community_already_disabled: Esta comunidad ya está deshabilitada
community_already_enabled: Esta comunidad ya está habilitada
Loading
Loading