feat: Add Telegram bot with group support

- Full Telegram bot implementation for Lightning Jackpot
- Commands: /start, /buy, /tickets, /wins, /address, /jackpot, /help
- Lightning invoice generation with QR codes
- Payment polling and confirmation notifications
- User state management (Redis/in-memory fallback)
- Group support with admin settings panel
- Configurable draw announcements and reminders
- Centralized messages for easy i18n
- Docker configuration included
This commit is contained in:
Michilis
2025-11-27 23:10:25 +00:00
parent d3bf8080b6
commit f743a6749c
29 changed files with 7616 additions and 1 deletions

View File

@@ -0,0 +1,145 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import config from '../config';
import { logger, logApiCall } from './logger';
import {
ApiResponse,
JackpotNextResponse,
BuyTicketsResponse,
TicketStatusResponse,
} from '../types';
class ApiClient {
private client: AxiosInstance;
constructor() {
this.client = axios.create({
baseURL: config.api.baseUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Response interceptor for logging
this.client.interceptors.response.use(
(response) => {
logApiCall(
response.config.url || '',
response.config.method?.toUpperCase() || 'GET',
response.status
);
return response;
},
(error: AxiosError) => {
logApiCall(
error.config?.url || '',
error.config?.method?.toUpperCase() || 'GET',
error.response?.status,
error.message
);
throw error;
}
);
}
/**
* Get next jackpot cycle information
*/
async getNextJackpot(): Promise<JackpotNextResponse | null> {
try {
const response = await this.client.get<ApiResponse<JackpotNextResponse>>(
'/jackpot/next'
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
if (status === 503) {
// No active lottery or cycle
return null;
}
}
logger.error('Failed to get next jackpot', { error });
throw error;
}
}
/**
* Buy lottery tickets
*/
async buyTickets(
tickets: number,
lightningAddress: string,
telegramUserId: number
): Promise<BuyTicketsResponse> {
try {
const response = await this.client.post<ApiResponse<BuyTicketsResponse>>(
'/jackpot/buy',
{
tickets,
lightning_address: lightningAddress,
buyer_name: `TG:${telegramUserId}`,
}
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorData = error.response?.data as ApiResponse<unknown>;
if (errorData?.error) {
throw new Error(errorData.message || errorData.error);
}
}
logger.error('Failed to buy tickets', { error });
throw error;
}
}
/**
* Get ticket purchase status
*/
async getTicketStatus(purchaseId: string): Promise<TicketStatusResponse | null> {
try {
const response = await this.client.get<ApiResponse<TicketStatusResponse>>(
`/tickets/${purchaseId}`
);
return response.data.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
logger.error('Failed to get ticket status', { error, purchaseId });
throw error;
}
}
/**
* Get user's ticket purchases by lightning address
* Note: This queries tickets by lightning address pattern matching
*/
async getUserTickets(
telegramUserId: number,
limit: number = 10,
offset: number = 0
): Promise<TicketStatusResponse[]> {
// Since the backend doesn't have Telegram-specific endpoints,
// we'll need to track purchases locally in state
// This is a placeholder for future backend integration
return [];
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
await this.client.get('/jackpot/next');
return true;
} catch (error) {
return false;
}
}
}
export const apiClient = new ApiClient();
export default apiClient;