Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/consumers/src/interfaces/bot.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ContextWithSession extends Context {
walletAction?: 'add' | 'remove';
walletsToRemove?: Set<string>;
awaitingWalletInput?: boolean;
fromStart?: boolean;
};
}

Expand Down
2 changes: 2 additions & 0 deletions apps/consumers/src/interfaces/slack-context.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface SlackBodyWithIds {
};
};
} | string; // Can be string for DialogSubmitAction or object for BlockAction
actions?: Array<{ action_id?: string; value?: string; type?: string }>;
}

/**
Expand All @@ -56,6 +57,7 @@ export interface SlackSession {
type: 'wallet' | 'dao';
action: 'add' | 'remove';
};
fromStart?: boolean;
}

/**
Expand Down
14 changes: 14 additions & 0 deletions apps/consumers/src/services/bot/slack-bot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export class SlackBotService implements BotServiceInterface {
// Welcome message actions
handlers.action('welcome_select_daos', async (ctx) => {
if (this.daoService) {
const channelId = ctx.body.channel?.id;
const workspaceId = ctx.body.team?.id || ctx.body.user?.team_id;
const fullUserId = `${workspaceId}:${channelId}`;
const hasDaos = await this.daoService.hasSubscriptions(fullUserId);
ctx.session.fromStart = !hasDaos;
await this.daoService.initialize(ctx);
}
});
Expand All @@ -74,13 +79,20 @@ export class SlackBotService implements BotServiceInterface {
handlers.action('dao_confirm_subscribe', async (ctx) => {
if (this.daoService) {
await this.daoService.confirm(ctx);

// If from onboarding flow, trigger wallet setup
if (ctx.session.fromStart && this.walletService) {
await this.walletService.showOnboardingWallet(ctx);
ctx.session.fromStart = false;
}
}
});

handlers.action('dao_checkboxes', async (ctx) => {
await ctx.ack();
});


handlers.action('wallet_checkboxes', async (ctx) => {
await ctx.ack();
});
Expand All @@ -94,6 +106,8 @@ export class SlackBotService implements BotServiceInterface {

handlers.action('wallet_add', async (ctx) => {
if (this.walletService) {
const firstAction = ctx.body.actions?.[0];
ctx.session.fromStart = firstAction?.value === 'onboarding';
await this.walletService.startAddWallet(ctx);
}
});
Expand Down
98 changes: 84 additions & 14 deletions apps/consumers/src/services/bot/telegram-bot.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { telegramMessages, uiMessages, ExplorerService, appendUtmParams } from '
import { TelegramDAOService } from '../dao/telegram-dao.service';
import { TelegramWalletService } from '../wallet/telegram-wallet.service';
import { EnsResolverService } from '../ens-resolver.service';
import { MatchedContext } from '../../interfaces/bot.interface';
import { ContextWithSession, MatchedContext } from '../../interfaces/bot.interface';
import { NotificationPayload } from '../../interfaces/notification.interface';
import { TelegramClientInterface } from '../../interfaces/telegram-client.interface';
import { BotServiceInterface } from '../../interfaces/bot-service.interface';
Expand Down Expand Up @@ -51,14 +51,11 @@ export class TelegramBotService implements BotServiceInterface {
private setupCommands(): void {
this.telegramClient.setupHandlers((handlers) => {
handlers.command(/^start$/i, async (ctx) => {
await ctx.reply(uiMessages.welcome, this.createPersistentKeyboard());
await this.replyStartFlow(ctx);
});

handlers.command(/^learn_more$/i, async (ctx) => {
await ctx.reply(uiMessages.help, {
parse_mode: 'HTML',
...this.createPersistentKeyboard()
});
await this.replyLearnMore(ctx);
});

handlers.command(/^daos$/i, async (ctx) => {
Expand All @@ -78,45 +75,78 @@ export class TelegramBotService implements BotServiceInterface {
});

handlers.hears(uiMessages.buttons.learnMore, async (ctx) => {
await ctx.reply(uiMessages.help, {
parse_mode: 'HTML',
...this.createPersistentKeyboard()
});
await this.replyLearnMore(ctx);
});

handlers.action(/^start$/, async (ctx) => {
await ctx.answerCbQuery();
if (ctx.session) ctx.session.fromStart = true;
await this.daoService.initialize(ctx);
});

handlers.action(/^dao_toggle_(\w+)$/, async (ctx) => {
await ctx.answerCbQuery();
const matchedCtx = ctx as MatchedContext;
const daoName = matchedCtx.match[1];
await this.daoService.toggle(ctx, daoName);
await ctx.answerCbQuery();
});

handlers.action(/^dao_confirm$/, async (ctx) => {
await ctx.answerCbQuery();
await this.daoService.confirm(ctx);
await this.triggerWalletFlowIfFromStart(ctx);
});

handlers.action(/^dao_select_all$/, async (ctx) => {
await ctx.answerCbQuery();
await this.daoService.selectAll(ctx);
});

handlers.action(/^dao_unselect_all$/, async (ctx) => {
await ctx.answerCbQuery();
await this.daoService.unselectAll(ctx);
});

// Wallet action handlers
handlers.action(/^wallet_add$/, async (ctx) => {
await this.walletService.addWallet(ctx);
await ctx.answerCbQuery();
await this.walletService.addWallet(ctx);
});

handlers.action(/^wallet_remove$/, async (ctx) => {
await this.walletService.removeWallet(ctx);
await ctx.answerCbQuery();
await this.walletService.removeWallet(ctx);
});

handlers.action(/^wallet_toggle_(.+)$/, async (ctx) => {
await ctx.answerCbQuery();
const matchedCtx = ctx as MatchedContext;
const address = matchedCtx.match[1];
await this.walletService.toggleWalletForRemoval(ctx, address);
await ctx.answerCbQuery();
});

handlers.action(/^wallet_confirm_remove$/, async (ctx) => {
await ctx.answerCbQuery();
await this.walletService.confirmRemoval(ctx);
});

handlers.action(/^learn_more_start$/, async (ctx) => {
await ctx.answerCbQuery();
await this.replyStartFlow(ctx);
});

handlers.action(/^learn_more_daos$/, async (ctx) => {
await ctx.answerCbQuery();
await this.daoService.initialize(ctx);
});

handlers.action(/^learn_more_wallets$/, async (ctx) => {
await ctx.answerCbQuery();
await this.walletService.initialize(ctx);
});

handlers.action(/^learn_more_settings$/, async (ctx) => {
await ctx.answerCbQuery(uiMessages.buttons.settingsComingSoon);
});

handlers.on('message', async (ctx, next) => {
Expand All @@ -134,6 +164,46 @@ export class TelegramBotService implements BotServiceInterface {
});
}

private async replyStartFlow(ctx: ContextWithSession): Promise<void> {
await ctx.reply(uiMessages.welcome, this.createPersistentKeyboard());
await ctx.reply(uiMessages.welcomeDao, {
reply_markup: {
inline_keyboard: [
[{ text: uiMessages.buttons.daos, callback_data: 'start' }]
]
}
});
}

private async replyLearnMore(ctx: ContextWithSession): Promise<void> {
await ctx.reply(uiMessages.help, {
parse_mode: 'HTML',
reply_markup: {
inline_keyboard: [
[
{ text: uiMessages.buttons.start, callback_data: 'learn_more_start' },
{ text: uiMessages.buttons.daos, callback_data: 'learn_more_daos' },
],
[
{ text: uiMessages.buttons.wallets, callback_data: 'learn_more_wallets' },
{ text: uiMessages.buttons.settings, callback_data: 'learn_more_settings' },
]
]
}
});
}

private async triggerWalletFlowIfFromStart(ctx: ContextWithSession): Promise<void> {
if (!ctx.session?.fromStart) return;
const user = ctx.from?.id;
if (user) {
const userWallets = await this.walletService.getUserWalletsWithDisplayNames(user.toString(), 'telegram');
if (!userWallets || userWallets.length === 0) {
await this.walletService.initialize(ctx, true);
}
}
}

async launch(): Promise<void> {
await this.telegramClient.launch();
}
Expand Down
8 changes: 8 additions & 0 deletions apps/consumers/src/services/dao/base-dao.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export abstract class BaseDAOService {
return await this.anticaptureClient.getDAOs();
}

/**
* Check if user has any DAO subscriptions
*/
public async hasSubscriptions(userId: string): Promise<boolean> {
const subs = await this.getUserSubscriptions(userId);
return subs.length > 0;
}

/**
* Get user's current DAO subscriptions
*/
Expand Down
51 changes: 33 additions & 18 deletions apps/consumers/src/services/dao/telegram-dao.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,16 @@ export class TelegramDAOService extends BaseDAOService {
* Toggle DAO selection when user clicks a button
*/
async toggle(ctx: ContextWithSession, daoName: string): Promise<void> {
const chatId = ctx.chat?.id;
const messageId = ctx.callbackQuery?.message?.message_id;
if (!chatId || !messageId) return;

this.ensureSession(ctx);

// Toggle selection in session
const normalizedDaoName = daoName.toUpperCase();
if (ctx.session.daoSelections.has(normalizedDaoName)) {
ctx.session.daoSelections.delete(normalizedDaoName);
} else {
ctx.session.daoSelections.add(normalizedDaoName);
}

try {
// Update inline keyboard to reflect new state
const daos = await this.fetchAvailableDAOs();
const keyboard = this.buildInlineKeyboard(daos, ctx.session.daoSelections);
await ctx.editMessageReplyMarkup(keyboard);
} catch (error) {
console.error('Error updating keyboard:', error);
await ctx.answerCbQuery(uiMessages.errors.updateFailed);
}
await this.refreshKeyboard(ctx);
}

/**
Expand Down Expand Up @@ -143,6 +130,32 @@ export class TelegramDAOService extends BaseDAOService {
await ctx.reply(uiMessages.errors.updateSubscriptionsFailed);
}
}

async selectAll(ctx: ContextWithSession): Promise<void> {
this.ensureSession(ctx);
const daos = await this.fetchAvailableDAOs();
daos.forEach(dao => ctx.session.daoSelections.add(dao.id.toUpperCase()));
await this.refreshKeyboard(ctx);
}

async unselectAll(ctx: ContextWithSession): Promise<void> {
this.ensureSession(ctx);
ctx.session.daoSelections.clear();
await this.refreshKeyboard(ctx);
}

/**
* Refresh the inline keyboard to reflect current session selections
*/
private async refreshKeyboard(ctx: ContextWithSession): Promise<void> {
try {
const daos = await this.fetchAvailableDAOs();
const keyboard = this.buildInlineKeyboard(daos, ctx.session.daoSelections);
await ctx.editMessageReplyMarkup(keyboard);
} catch (error) {
console.error('Error updating keyboard:', error);
}
}

/**
* Build Telegram inline keyboard for DAO selection
Expand All @@ -160,7 +173,6 @@ export class TelegramDAOService extends BaseDAOService {
};
});

// Group buttons into rows of 4
const BUTTONS_PER_ROW = 3;
const daoButtonRows: any[][] = [];

Expand All @@ -171,9 +183,12 @@ export class TelegramDAOService extends BaseDAOService {
return {
inline_keyboard: [
...daoButtonRows,
// Confirm button row
[
{ text: uiMessages.confirmSelection, callback_data: 'dao_confirm' }
{ text: uiMessages.buttons.selectAll, callback_data: 'dao_select_all' },
{ text: uiMessages.buttons.unselectAll, callback_data: 'dao_unselect_all' },
],
[
{ text: uiMessages.confirmSelection, callback_data: 'dao_confirm' },
]
]
};
Expand All @@ -182,7 +197,7 @@ export class TelegramDAOService extends BaseDAOService {
/**
* Show confirmation message after updating subscriptions
*/
private async showConfirmationMessage(ctx: any, selectedDAOs: Set<string>): Promise<void> {
private async showConfirmationMessage(ctx: ContextWithSession, selectedDAOs: Set<string>): Promise<void> {
if (selectedDAOs.size > 0) {
const daoList = this.formatDAOListWithBullets(selectedDAOs);

Expand Down
Loading