Skip to content

Commit dcaeeb8

Browse files
Merge pull request #157 from blockful/refactor/lockfile
Refactor/lockfile
2 parents 115016a + 479f83e commit dcaeeb8

66 files changed

Lines changed: 5015 additions & 930 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/consumers/jest.config.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
/** @type {import('jest').Config} */
21
module.exports = {
32
preset: 'ts-jest',
43
testEnvironment: 'node',
5-
roots: ['<rootDir>/src'],
64
testMatch: ['**/*.test.ts'],
7-
collectCoverageFrom: [
8-
'src/**/*.ts',
9-
'!src/**/*.d.ts',
10-
'!src/**/index.ts',
11-
],
12-
moduleNameMapper: {
13-
'^@/(.*)$': '<rootDir>/src/$1',
14-
},
5+
transform: {
6+
'^.+\\.tsx?$': ['ts-jest', {
7+
isolatedModules: true
8+
}]
9+
}
1510
};

apps/consumers/src/app.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { Telegraf } from 'telegraf';
2-
import { session } from 'telegraf/session';
31
import { AxiosInstance } from 'axios';
42
import { TelegramBotService } from './services/telegram-bot.service';
53
import { DAOService } from './services/dao.service';
@@ -8,29 +6,34 @@ import { ExplorerService } from './services/explorer.service';
86
import { EnsResolverService } from './services/ens-resolver.service';
97
import { AnticaptureClient } from '@notification-system/anticapture-client';
108
import { SubscriptionAPIService } from './services/subscription-api.service';
11-
import { ContextWithSession } from './interfaces/bot.interface';
129
import { RabbitMQNotificationConsumerService } from './services/rabbitmq-notification-consumer.service';
10+
import { TelegramClientInterface } from './interfaces/telegram-client.interface';
1311

1412
export class App {
1513
private telegramBotService: TelegramBotService;
1614
private rabbitmqConsumerService?: RabbitMQNotificationConsumerService;
1715
private rabbitmqUrl: string;
1816

1917
constructor(
20-
telegramBotToken: string,
2118
subscriptionServerUrl: string,
2219
httpClient: AxiosInstance,
2320
rabbitmqUrl: string,
24-
ensResolver: EnsResolverService
21+
ensResolver: EnsResolverService,
22+
telegramClient: TelegramClientInterface
2523
) {
2624
const subscriptionApi = new SubscriptionAPIService(subscriptionServerUrl);
2725
const anticaptureClient = new AnticaptureClient(httpClient);
2826
const daoService = new DAOService(anticaptureClient, subscriptionApi);
2927
const walletService = new WalletService(subscriptionApi, ensResolver);
3028
const explorerService = new ExplorerService();
31-
const bot = new Telegraf<ContextWithSession>(telegramBotToken);
32-
bot.use(session());
33-
this.telegramBotService = new TelegramBotService(bot, daoService, walletService, explorerService, ensResolver);
29+
30+
this.telegramBotService = new TelegramBotService(
31+
telegramClient,
32+
daoService,
33+
walletService,
34+
explorerService,
35+
ensResolver
36+
);
3437
this.rabbitmqUrl = rabbitmqUrl;
3538
}
3639

apps/consumers/src/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,23 @@ import axios from 'axios';
1414
import { App } from './app';
1515
import { loadConfig } from './config/env';
1616
import { EnsResolverService } from './services/ens-resolver.service';
17+
import { TelegramClient } from './telegram.client';
1718

1819
const config = loadConfig();
1920

2021
// Create ENS resolver
2122
const ensResolver = new EnsResolverService();
2223

24+
// Create Telegram client for production
25+
const telegramClient = new TelegramClient(config.telegramBotToken);
26+
2327
// Create and start the application
2428
const app = new App(
25-
config.telegramBotToken,
2629
config.subscriptionServerUrl,
2730
axios.create({ baseURL: config.anticaptureGraphqlEndpoint }),
2831
config.rabbitmqUrl,
29-
ensResolver
32+
ensResolver,
33+
telegramClient
3034
);
3135

3236
(async () => {

apps/consumers/src/interfaces/bot.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ export interface ContextWithSession extends Context {
1212
awaitingWalletInput?: boolean;
1313
};
1414
}
15+
16+
/**
17+
* Context with regex match for action handlers
18+
*/
19+
export type MatchedContext = ContextWithSession & {
20+
match: RegExpExecArray;
21+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Interface for Telegram client operations
3+
* Abstracts the Telegram API to allow for testing and different implementations
4+
*/
5+
6+
import { Message } from 'telegraf/types';
7+
import { ContextWithSession, MatchedContext } from '../interfaces/bot.interface';
8+
9+
export interface SendMessageOptions {
10+
parse_mode?: 'Markdown' | 'HTML' | 'MarkdownV2';
11+
reply_markup?: any;
12+
[key: string]: any;
13+
}
14+
15+
export interface HandlerRegistration {
16+
command(command: string | RegExp, handler: (ctx: ContextWithSession) => Promise<void>): void;
17+
hears(text: string | RegExp, handler: (ctx: ContextWithSession) => Promise<void>): void;
18+
action(action: string | RegExp, handler: (ctx: ContextWithSession | MatchedContext) => Promise<void>): void;
19+
on(event: string, handler: (ctx: ContextWithSession, next: () => Promise<void>) => Promise<void>): void;
20+
use(middleware: any): void;
21+
}
22+
23+
export interface TelegramClientInterface {
24+
/**
25+
* Send a message to a specific chat
26+
* @param chatId The chat identifier
27+
* @param text The message text
28+
* @param options Additional options for the message
29+
* @returns The sent message
30+
*/
31+
sendMessage(
32+
chatId: string | number,
33+
text: string,
34+
options?: SendMessageOptions
35+
): Promise<Message.TextMessage>;
36+
37+
/**
38+
* Setup handlers for bot commands and interactions
39+
* @param registration Handler registration callback
40+
*/
41+
setupHandlers(registration: (handlers: HandlerRegistration) => void): void;
42+
43+
/**
44+
* Launch the bot (start polling or webhook)
45+
* @returns Promise that resolves when bot is running
46+
*/
47+
launch(): Promise<void>;
48+
49+
/**
50+
* Stop the bot
51+
* @param signal The signal that triggered the stop
52+
*/
53+
stop(signal?: string): void;
54+
55+
/**
56+
* Check if the bot is running
57+
* @returns true if bot is active
58+
*/
59+
isRunning(): boolean;
60+
}

apps/consumers/src/services/telegram-bot.service.ts

Lines changed: 80 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,31 @@
44
* Consolidates both interactive commands and notification sending capabilities.
55
*/
66

7-
import { Telegraf, Markup } from 'telegraf';
7+
import { Markup } from 'telegraf';
88
import { WELCOME_MESSAGE, HELP_MESSAGE, DAOS_BUTTON_TEXT, LEARN_MORE_BUTTON_TEXT, MY_WALLETS_BUTTON_TEXT } from '../messages';
99
import { DAOService } from '../services/dao.service';
1010
import { WalletService } from '../services/wallet.service';
1111
import { ExplorerService } from '../services/explorer.service';
1212
import { EnsResolverService } from '../services/ens-resolver.service';
13-
import { ContextWithSession } from '../interfaces/bot.interface';
13+
import { ContextWithSession, MatchedContext } from '../interfaces/bot.interface';
1414
import { NotificationPayload } from '../interfaces/notification.interface';
15+
import { TelegramClientInterface } from '../interfaces/telegram-client.interface';
1516

1617
export class TelegramBotService {
17-
private bot: Telegraf<ContextWithSession>;
18+
private telegramClient: TelegramClientInterface;
1819
private daoService: DAOService;
1920
private walletService: WalletService;
2021
private explorerService: ExplorerService;
2122
private ensResolver: EnsResolverService;
2223

2324
constructor(
24-
bot: Telegraf<ContextWithSession>,
25+
telegramClient: TelegramClientInterface,
2526
daoService: DAOService,
2627
walletService: WalletService,
2728
explorerService: ExplorerService,
2829
ensResolver: EnsResolverService
2930
) {
30-
this.bot = bot;
31+
this.telegramClient = telegramClient;
3132
this.daoService = daoService;
3233
this.walletService = walletService;
3334
this.explorerService = explorerService;
@@ -48,75 +49,78 @@ export class TelegramBotService {
4849
}
4950

5051
private setupCommands(): void {
51-
this.bot.command(/^start$/i, async (ctx) => {
52-
await ctx.reply(WELCOME_MESSAGE, this.createPersistentKeyboard());
53-
});
52+
this.telegramClient.setupHandlers((handlers) => {
53+
handlers.command(/^start$/i, async (ctx) => {
54+
await ctx.reply(WELCOME_MESSAGE, this.createPersistentKeyboard());
55+
});
5456

55-
this.bot.command(/^learn_more$/i, async (ctx) => {
56-
await ctx.reply(HELP_MESSAGE, {
57-
parse_mode: 'HTML',
58-
...this.createPersistentKeyboard()
57+
handlers.command(/^learn_more$/i, async (ctx) => {
58+
await ctx.reply(HELP_MESSAGE, {
59+
parse_mode: 'HTML',
60+
...this.createPersistentKeyboard()
61+
});
5962
});
60-
});
6163

62-
this.bot.command(/^daos$/i, async (ctx) => {
63-
await this.daoService.initialize(ctx);
64-
});
64+
handlers.command(/^daos$/i, async (ctx) => {
65+
await this.daoService.initialize(ctx);
66+
});
6567

66-
this.bot.command(/^wallets$/i, async (ctx) => {
67-
await this.walletService.initialize(ctx);
68-
});
68+
handlers.command(/^wallets$/i, async (ctx) => {
69+
await this.walletService.initialize(ctx);
70+
});
6971

70-
this.bot.hears(DAOS_BUTTON_TEXT, async (ctx) => {
71-
await this.daoService.initialize(ctx);
72-
});
72+
handlers.hears(DAOS_BUTTON_TEXT, async (ctx) => {
73+
await this.daoService.initialize(ctx);
74+
});
7375

74-
this.bot.hears(MY_WALLETS_BUTTON_TEXT, async (ctx) => {
75-
await this.walletService.initialize(ctx);
76-
});
76+
handlers.hears(MY_WALLETS_BUTTON_TEXT, async (ctx) => {
77+
await this.walletService.initialize(ctx);
78+
});
7779

78-
this.bot.hears(LEARN_MORE_BUTTON_TEXT, async (ctx) => {
79-
await ctx.reply(HELP_MESSAGE, {
80-
parse_mode: 'HTML',
81-
...this.createPersistentKeyboard()
80+
handlers.hears(LEARN_MORE_BUTTON_TEXT, async (ctx) => {
81+
await ctx.reply(HELP_MESSAGE, {
82+
parse_mode: 'HTML',
83+
...this.createPersistentKeyboard()
84+
});
8285
});
83-
});
8486

85-
this.bot.action(/^dao_toggle_(\w+)$/, async (ctx) => {
86-
const daoName = ctx.match[1];
87-
await this.daoService.toggle(ctx, daoName);
88-
await ctx.answerCbQuery();
89-
});
87+
handlers.action(/^dao_toggle_(\w+)$/, async (ctx) => {
88+
const matchedCtx = ctx as MatchedContext;
89+
const daoName = matchedCtx.match[1];
90+
await this.daoService.toggle(ctx, daoName);
91+
await ctx.answerCbQuery();
92+
});
9093

91-
this.bot.action(/^dao_confirm$/, async (ctx) => {
92-
await this.daoService.confirm(ctx);
93-
await ctx.answerCbQuery();
94-
});
94+
handlers.action(/^dao_confirm$/, async (ctx) => {
95+
await this.daoService.confirm(ctx);
96+
await ctx.answerCbQuery();
97+
});
9598

96-
// Wallet action handlers
97-
this.bot.action(/^wallet_add$/, async (ctx) => {
98-
await this.walletService.addWallet(ctx);
99-
await ctx.answerCbQuery();
100-
});
99+
// Wallet action handlers
100+
handlers.action(/^wallet_add$/, async (ctx) => {
101+
await this.walletService.addWallet(ctx);
102+
await ctx.answerCbQuery();
103+
});
101104

102-
this.bot.action(/^wallet_remove$/, async (ctx) => {
103-
await this.walletService.removeWallet(ctx);
104-
await ctx.answerCbQuery();
105-
});
105+
handlers.action(/^wallet_remove$/, async (ctx) => {
106+
await this.walletService.removeWallet(ctx);
107+
await ctx.answerCbQuery();
108+
});
106109

107-
this.bot.action(/^wallet_toggle_(.+)$/, async (ctx) => {
108-
const address = ctx.match[1];
109-
await this.walletService.toggleWalletForRemoval(ctx, address);
110-
await ctx.answerCbQuery();
111-
});
110+
handlers.action(/^wallet_toggle_(.+)$/, async (ctx) => {
111+
const matchedCtx = ctx as MatchedContext;
112+
const address = matchedCtx.match[1];
113+
await this.walletService.toggleWalletForRemoval(ctx, address);
114+
await ctx.answerCbQuery();
115+
});
112116

113-
this.bot.action(/^wallet_confirm_remove$/, async (ctx) => {
114-
await this.walletService.confirmRemoval(ctx);
115-
await ctx.answerCbQuery();
116-
});
117+
handlers.action(/^wallet_confirm_remove$/, async (ctx) => {
118+
await this.walletService.confirmRemoval(ctx);
119+
await ctx.answerCbQuery();
120+
});
117121

118-
this.bot.on('message', async (ctx, next) => {
119-
if ('text' in ctx.message && !ctx.message.text.startsWith('/')) {
122+
handlers.on('message', async (ctx, next) => {
123+
if (ctx.message && 'text' in ctx.message && !ctx.message.text.startsWith('/')) {
120124
if (ctx.session?.awaitingWalletInput) {
121125
await this.walletService.processWalletInput(ctx, ctx.message.text);
122126
return;
@@ -126,16 +130,16 @@ export class TelegramBotService {
126130
this.createPersistentKeyboard());
127131
}
128132
return next();
133+
});
129134
});
130135
}
131136

132137
async launch(): Promise<void> {
133-
await this.bot.launch();
134-
console.log('🤖 Bot is running...');
138+
await this.telegramClient.launch();
135139
}
136140

137141
public stop(signal: string): void {
138-
this.bot.stop(signal);
142+
this.telegramClient.stop(signal);
139143
}
140144

141145
/**
@@ -147,12 +151,15 @@ export class TelegramBotService {
147151
public async sendNotification(payload: NotificationPayload): Promise<string> {
148152
let processedMessage = payload.message;
149153

150-
// Process transaction link if transaction metadata is provided
151-
if (payload.metadata?.transaction) {
152-
const { hash, chainId } = payload.metadata.transaction;
153-
const txUrl = this.explorerService.getTransactionLink(chainId, hash);
154-
const markdownLink = `[Transaction details](${txUrl})`;
155-
processedMessage = processedMessage.replace('{{txLink}}', markdownLink);
154+
// Process transaction link placeholder
155+
if (processedMessage.includes('{{txLink}}')) {
156+
const txUrl = payload.metadata?.transaction
157+
? this.explorerService.getTransactionLink(payload.metadata.transaction.chainId, payload.metadata.transaction.hash)
158+
: null;
159+
160+
processedMessage = txUrl
161+
? processedMessage.replace('{{txLink}}', `[Transaction details](${txUrl})`)
162+
: processedMessage.replace('\n\n{{txLink}}', '');
156163
}
157164

158165
// Process ENS names if addresses are provided in metadata
@@ -163,10 +170,13 @@ export class TelegramBotService {
163170
}
164171
}
165172

166-
const sentMessage = await this.bot.telegram.sendMessage(
173+
const sentMessage = await this.telegramClient.sendMessage(
167174
payload.channelUserId,
168175
processedMessage,
169-
{ parse_mode: 'Markdown' }
176+
{
177+
parse_mode: 'Markdown',
178+
disable_web_page_preview: true
179+
}
170180
);
171181
return `${sentMessage.message_id}`;
172182
}

0 commit comments

Comments
 (0)