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:
224
telegram_bot/src/services/groupState.ts
Normal file
224
telegram_bot/src/services/groupState.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import Redis from 'ioredis';
|
||||
import config from '../config';
|
||||
import { logger } from './logger';
|
||||
import { GroupSettings, DEFAULT_GROUP_SETTINGS } from '../types/groups';
|
||||
|
||||
const GROUP_PREFIX = 'tg_group:';
|
||||
const GROUPS_LIST_KEY = 'tg_groups_list';
|
||||
const STATE_TTL = 60 * 60 * 24 * 365; // 1 year
|
||||
|
||||
class GroupStateManager {
|
||||
private redis: Redis | null = null;
|
||||
private memoryStore: Map<string, string> = new Map();
|
||||
private useRedis: boolean = false;
|
||||
|
||||
async init(redisUrl: string | null): Promise<void> {
|
||||
if (redisUrl) {
|
||||
try {
|
||||
this.redis = new Redis(redisUrl);
|
||||
await this.redis.ping();
|
||||
this.useRedis = true;
|
||||
logger.info('Group state manager initialized with Redis');
|
||||
} catch (error) {
|
||||
logger.warn('Failed to connect to Redis for groups, using in-memory store');
|
||||
this.redis = null;
|
||||
this.useRedis = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async get(key: string): Promise<string | null> {
|
||||
if (this.useRedis && this.redis) {
|
||||
return await this.redis.get(key);
|
||||
}
|
||||
return this.memoryStore.get(key) || null;
|
||||
}
|
||||
|
||||
private async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
if (this.useRedis && this.redis) {
|
||||
if (ttl) {
|
||||
await this.redis.setex(key, ttl, value);
|
||||
} else {
|
||||
await this.redis.set(key, value);
|
||||
}
|
||||
} else {
|
||||
this.memoryStore.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private async del(key: string): Promise<void> {
|
||||
if (this.useRedis && this.redis) {
|
||||
await this.redis.del(key);
|
||||
} else {
|
||||
this.memoryStore.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
private async sadd(key: string, value: string): Promise<void> {
|
||||
if (this.useRedis && this.redis) {
|
||||
await this.redis.sadd(key, value);
|
||||
} else {
|
||||
const existing = this.memoryStore.get(key);
|
||||
const set = existing ? new Set(JSON.parse(existing)) : new Set();
|
||||
set.add(value);
|
||||
this.memoryStore.set(key, JSON.stringify([...set]));
|
||||
}
|
||||
}
|
||||
|
||||
private async srem(key: string, value: string): Promise<void> {
|
||||
if (this.useRedis && this.redis) {
|
||||
await this.redis.srem(key, value);
|
||||
} else {
|
||||
const existing = this.memoryStore.get(key);
|
||||
if (existing) {
|
||||
const set = new Set(JSON.parse(existing));
|
||||
set.delete(value);
|
||||
this.memoryStore.set(key, JSON.stringify([...set]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async smembers(key: string): Promise<string[]> {
|
||||
if (this.useRedis && this.redis) {
|
||||
return await this.redis.smembers(key);
|
||||
}
|
||||
const existing = this.memoryStore.get(key);
|
||||
return existing ? JSON.parse(existing) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group settings
|
||||
*/
|
||||
async getGroup(groupId: number): Promise<GroupSettings | null> {
|
||||
const key = `${GROUP_PREFIX}${groupId}`;
|
||||
const data = await this.get(key);
|
||||
if (!data) return null;
|
||||
|
||||
try {
|
||||
const settings = JSON.parse(data);
|
||||
return {
|
||||
...settings,
|
||||
addedAt: new Date(settings.addedAt),
|
||||
updatedAt: new Date(settings.updatedAt),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse group settings', { groupId, error });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update group settings
|
||||
*/
|
||||
async saveGroup(settings: GroupSettings): Promise<void> {
|
||||
const key = `${GROUP_PREFIX}${settings.groupId}`;
|
||||
settings.updatedAt = new Date();
|
||||
await this.set(key, JSON.stringify(settings), STATE_TTL);
|
||||
await this.sadd(GROUPS_LIST_KEY, settings.groupId.toString());
|
||||
logger.debug('Group settings saved', { groupId: settings.groupId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new group when bot is added
|
||||
*/
|
||||
async registerGroup(
|
||||
groupId: number,
|
||||
groupTitle: string,
|
||||
addedBy: number
|
||||
): Promise<GroupSettings> {
|
||||
const existing = await this.getGroup(groupId);
|
||||
|
||||
if (existing) {
|
||||
// Update title if changed
|
||||
existing.groupTitle = groupTitle;
|
||||
existing.updatedAt = new Date();
|
||||
await this.saveGroup(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
const settings: GroupSettings = {
|
||||
groupId,
|
||||
groupTitle,
|
||||
...DEFAULT_GROUP_SETTINGS,
|
||||
addedBy,
|
||||
addedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
await this.saveGroup(settings);
|
||||
logger.info('New group registered', { groupId, groupTitle, addedBy });
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove group when bot is removed
|
||||
*/
|
||||
async removeGroup(groupId: number): Promise<void> {
|
||||
const key = `${GROUP_PREFIX}${groupId}`;
|
||||
await this.del(key);
|
||||
await this.srem(GROUPS_LIST_KEY, groupId.toString());
|
||||
logger.info('Group removed', { groupId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific setting
|
||||
*/
|
||||
async updateSetting(
|
||||
groupId: number,
|
||||
setting: keyof Pick<GroupSettings, 'enabled' | 'drawAnnouncements' | 'reminders' | 'ticketPurchaseAllowed'>,
|
||||
value: boolean
|
||||
): Promise<GroupSettings | null> {
|
||||
const settings = await this.getGroup(groupId);
|
||||
if (!settings) return null;
|
||||
|
||||
settings[setting] = value;
|
||||
await this.saveGroup(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups with a specific feature enabled
|
||||
*/
|
||||
async getGroupsWithFeature(
|
||||
feature: 'enabled' | 'drawAnnouncements' | 'reminders'
|
||||
): Promise<GroupSettings[]> {
|
||||
const groupIds = await this.smembers(GROUPS_LIST_KEY);
|
||||
const groups: GroupSettings[] = [];
|
||||
|
||||
for (const id of groupIds) {
|
||||
const settings = await this.getGroup(parseInt(id, 10));
|
||||
if (settings && settings.enabled && settings[feature]) {
|
||||
groups.push(settings);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered groups
|
||||
*/
|
||||
async getAllGroups(): Promise<GroupSettings[]> {
|
||||
const groupIds = await this.smembers(GROUPS_LIST_KEY);
|
||||
const groups: GroupSettings[] = [];
|
||||
|
||||
for (const id of groupIds) {
|
||||
const settings = await this.getGroup(parseInt(id, 10));
|
||||
if (settings) {
|
||||
groups.push(settings);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.redis) {
|
||||
await this.redis.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const groupStateManager = new GroupStateManager();
|
||||
export default groupStateManager;
|
||||
|
||||
Reference in New Issue
Block a user