1
0
mirror of https://github.com/sadfsdfdsa/allbot.git synced 2024-11-19 00:31:42 +02:00

feat(refactoring): big refactoring for bot commands

This commit is contained in:
Artem Shuvaev 2023-10-03 17:43:53 +05:00
parent 353b03df56
commit fc98bf3c06
10 changed files with 319 additions and 242 deletions

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": true,
"semi": false,
"singleQuote": true
}

View File

@ -1,30 +1,15 @@
import { Context, Telegraf } from 'telegraf'
import { Telegraf } from 'telegraf'
import { UserRepository } from './repository.js'
import { MetricsService } from './metrics.js'
import { message } from 'telegraf/filters'
import { Chat, Message, User } from 'telegraf/types'
import { Chat, User } from 'telegraf/types'
import { isChatGroup } from './utils/utils.js'
type HandleMessagePayload = {
chatId: Chat['id']
text: string
from: NonNullable<Message['from']>
messageId: Message['message_id']
}
export class Bot {
private bot: Telegraf
private readonly MENTION_COMMANDS = ['@all', '/all']
private readonly FEEDBACK_COMMAND = '/feedback'
private readonly CODE_COMMAND = '/code'
private readonly SUPPORT_PAY_COMMAND = '/support'
private readonly PRIVACY_COMMAND = '/privacy'
private readonly ADMIN_ID: number | undefined
private isListening = false
@ -44,22 +29,36 @@ export class Bot {
this.bot = new Telegraf(token)
this.bot.on(message('text'), async (ctx) => {
const {
message: { from, text, message_id },
chat: { id },
} = ctx
this.bot.telegram.setMyCommands([
{
command: 'all',
description: 'Mention all users in a group',
},
{
command: 'donate',
description: 'Get crypto payments links for supporting the project',
},
{
command: 'feedback',
description: 'Send feedback or bug reports (English please)',
},
{
command: 'code',
description: 'Get link for bot opensource code',
},
{
command: 'privacy',
description: 'How the Bot takes care of your personal data',
},
])
await this.handleMessage(
{
from,
text,
messageId: message_id,
chatId: id,
},
(...args: Parameters<Context['sendMessage']>) => ctx.reply(...args),
)
})
this.registerDonateCommand()
this.registerFeedbackCommand()
this.registerPrivacyCommand()
this.registerCodeCommand()
// Should be last for not overriding commands below
this.registerHandleMessage()
this.bot.on(message('new_chat_members'), ({ message, chat }) =>
this.handleAddMembers(chat.id, message.new_chat_members)
@ -83,31 +82,8 @@ export class Bot {
this.bot.launch()
}
private async handleMessage(
{ from, text, messageId, chatId }: HandleMessagePayload,
reply: Context['reply'],
): Promise<void> {
if (text.startsWith(this.PRIVACY_COMMAND)) {
console.log('Send privacy policy')
const message = `
Are you concerned about your security and personal data? This is right!
What do we use? Identifiers of your groups to store data about participants in them: usernames and identifiers to correctly call all users of the group.
All data is transmitted only via encrypted channels and is not used for other purposes.
We don't read your messages, don't log data about you in public systems and 3th party services except safe hosting and database.
You can view the project's codebase using ${this.CODE_COMMAND}.
Be careful when using unfamiliar bots in your communication, it can be dangerous!
`
reply(message, {
reply_to_message_id: messageId,
parse_mode: 'HTML'
})
return
}
if (text.startsWith(this.SUPPORT_PAY_COMMAND)) {
private registerDonateCommand(): void {
this.bot.command('donate', (ctx) => {
console.log('Send payments info')
const message = `
@ -118,82 +94,141 @@ Support via USDT-TRX: <code>TJyEa6p3HvAHz34gn7hZHYNwY65iHryu3w</code>
Support via USDT-ETH: <code>0x7f49e01c13fE782aEB06Dc35a37d357b955b67B0</code>
Support via BTC: <code>bc1qgmq6033fnte2ata8ku3zgvj0n302zvr9cexcng</code>
Thank you for using and help!
Note, than you can send ${this.FEEDBACK_COMMAND} with features or problems.
Note, than you can send /feedback with features or problems.
`
reply(message, {
reply_to_message_id: messageId,
parse_mode: 'HTML'
this.metricsService.updateLatestPaymentsCall(`${ctx.chat.id}`)
ctx.reply(message, {
reply_to_message_id: ctx.message.message_id,
parse_mode: 'HTML',
})
})
}
return
}
private registerFeedbackCommand(): void {
this.bot.command('feedback', (ctx) => {
const {
message: { from, text, message_id: messageId },
chat: { id: chatId },
} = ctx
if (text.startsWith(this.CODE_COMMAND)) {
console.log('Send code info')
console.log(ctx.message)
reply(`I am an opensource project, feel free to reuse code or make bot better via /feedback.\nGithub link: https://github.com/sadfsdfdsa/allbot`, {
reply_to_message_id: messageId,
})
return
}
if (text.startsWith(this.FEEDBACK_COMMAND)) {
const feedback = text.split(this.FEEDBACK_COMMAND)[1] || undefined
const feedback = text.split('/feedback')[1] || undefined
if (!feedback) {
console.log(`Receive empty feedback from user ${from.username} in ${chatId}: ${feedback}`)
console.log(
`Receive empty feedback from user ${from.username} in ${chatId}: ${feedback}`
)
reply(`Add something in your feedback as feature or bug report`, {
ctx.reply(`Add something in your feedback as feature or bug report`, {
reply_to_message_id: messageId,
})
return
}
console.log(`Receive feedback from user ${from.username} in ${chatId}: ${feedback}`)
console.log(
`Receive feedback from user ${from.username} in ${chatId}: ${feedback}`
)
reply(`Your review has been successfully registered, we will contact you, thank you!`, {
reply_to_message_id: messageId,
})
ctx.reply(
`Your review has been successfully registered, we will contact you, thank you!`,
{
reply_to_message_id: messageId,
}
)
if (!this.ADMIN_ID) return
this.bot.telegram.sendMessage(this.ADMIN_ID, `There is a new feedback from @${from.username} in chat group ${chatId}:\n${feedback}`)
return
}
this.bot.telegram.sendMessage(
this.ADMIN_ID,
`There is a new feedback from @${from.username} in chat group ${chatId}:\n${feedback}`
)
})
}
if (!isChatGroup(chatId)) {
console.log('Direct message from', from.username)
private registerPrivacyCommand(): void {
this.bot.command('privacy', (ctx) => {
console.log('Send privacy policy')
reply(`Add me to your group, here is example @all mention for you:`)
const message = `
Are you concerned about your security and personal data? This is right!
What do we use? Identifiers of your groups to store data about participants in them: usernames and identifiers to correctly call all users of the group.
All data is transmitted only via encrypted channels and is not used for other purposes.
We don't read your messages, don't log data about you in public systems and 3th party services except safe hosting and database.
You can view the project's codebase using /code.
Be careful when using unfamiliar bots in your communication, it can be dangerous!
`
reply(`All from ${from.username}: @${from.username}`, {
ctx.reply(message, {
reply_to_message_id: ctx.message.message_id,
parse_mode: 'HTML',
})
})
}
private registerCodeCommand(): void {
this.bot.command('privacy', (ctx) => {
console.log('Send code info')
ctx.reply(
`I am an opensource project, feel free to reuse code or make bot better via /feedback.\nGithub link: https://github.com/sadfsdfdsa/allbot`,
{
reply_to_message_id: ctx.message.message_id,
}
)
})
}
private registerHandleMessage(): void {
this.bot.on(message('text'), async (ctx) => {
const {
message: { from, text, message_id: messageId },
chat: { id: chatId },
} = ctx
if (!isChatGroup(chatId)) {
console.log('Direct message from', from.username)
ctx.reply(`Add me to your group, here is example @all mention for you:`)
ctx.reply(`All from ${from.username}: @${from.username}`, {
reply_to_message_id: messageId,
})
return
}
await this.userRepository.addUsers(chatId, [from])
const isCallAll = this.MENTION_COMMANDS.some((command) =>
text.includes(command)
)
if (!isCallAll) return
console.log(`Mention with pattern in group`, chatId)
const chatUsernames = await this.userRepository.getUsernamesByChatId(
chatId
)
if (!Object.values(chatUsernames).length) return
const str = Object.values(chatUsernames).map(
(username) => `@${username} `
)
this.metricsService.addReply()
ctx.reply(`All from ${from.username}: ${str}`, {
reply_to_message_id: messageId,
})
return
}
await this.userRepository.addUsers(chatId, [from])
const isCallAll = this.MENTION_COMMANDS.some((command) => text.includes(command))
if (!isCallAll) return
console.log(`Mention with pattern in group`, chatId)
const chatUsernames = await this.userRepository.getUsernamesByChatId(chatId)
if (!Object.values(chatUsernames).length) return
const str = Object.values(chatUsernames).map((username) => `@${username} `)
this.metricsService.addReply()
reply(`All from ${from.username}: ${str}`, {
reply_to_message_id: messageId,
})
}
private handleAddMembers(chatId: Chat['id'], users: User[]): Promise<void> {
console.log('Try add new members', users.map(item => item.username))
console.log(
'Try add new members',
users.map((item) => item.username)
)
return this.userRepository.addUsers(chatId, users)
}

View File

@ -3,47 +3,47 @@ import { CacheService } from './cache'
describe('CacheService', () => {
describe('#addToCache', () => {
test('should correct add unique values', async () => {
const instance = new CacheService(10)
const instance = new CacheService(10)
const first = instance.addToCache(['test'])
const second = instance.addToCache(['test'])
const first = instance.addToCache(['test'])
const second = instance.addToCache(['test'])
expect(first).toBe(1)
expect(second).toBe(1)
expect(first).toBe(1)
expect(second).toBe(1)
})
})
describe('#isInCache', () => {
test('should correct check is in cache', async () => {
const instance = new CacheService(10)
const instance = new CacheService(10)
instance.addToCache(['test', 'test2', 'test'])
instance.addToCache(['test', 'test2', 'test'])
const res1 = instance.isInCache('test')
const res2 = instance.isInCache('test2')
const res3 = instance.isInCache('test3')
const res1 = instance.isInCache('test')
const res2 = instance.isInCache('test2')
const res3 = instance.isInCache('test3')
expect(res1).toBe(true)
expect(res2).toBe(true)
expect(res3).toBe(false)
expect(res1).toBe(true)
expect(res2).toBe(true)
expect(res3).toBe(false)
})
})
describe('#tryClearCache', () => {
test('should remove element from the start of the cached array', async () => {
const instance = new CacheService(2)
const instance = new CacheService(2)
instance.addToCache(['test', 'test2', 'test3'])
instance.addToCache(['test', 'test2', 'test3'])
instance.tryClearCache()
instance.tryClearCache()
const res1 = instance.isInCache('test')
const res2 = instance.isInCache('test2')
const res3 = instance.isInCache('test3')
const res1 = instance.isInCache('test')
const res2 = instance.isInCache('test2')
const res3 = instance.isInCache('test3')
expect(res1).toBe(false)
expect(res2).toBe(true)
expect(res3).toBe(true)
expect(res1).toBe(false)
expect(res2).toBe(true)
expect(res3).toBe(true)
})
})
})

View File

@ -1,38 +1,36 @@
import { User } from "telegraf/types";
import { User } from 'telegraf/types'
type EnsuredUsername = NonNullable<User['username']>
export class CacheService {
private readonly cachedUsernames = new Array<EnsuredUsername>()
private readonly cachedUsernames = new Array<EnsuredUsername>()
constructor(
private readonly MAX_CACHE_SIZE = 1000
) {
console.log('Init Cache service')
}
constructor(private readonly MAX_CACHE_SIZE = 1000) {
console.log('Init Cache service')
}
public isInCache(username: EnsuredUsername): boolean {
return this.cachedUsernames.includes(username)
}
public isInCache(username: EnsuredUsername): boolean {
return this.cachedUsernames.includes(username)
}
public addToCache(usernames: EnsuredUsername[]): number {
usernames.forEach(this.addToCacheSingle.bind(this))
public addToCache(usernames: EnsuredUsername[]): number {
usernames.forEach(this.addToCacheSingle.bind(this))
return this.cachedUsernames.length
}
return this.cachedUsernames.length
}
public tryClearCache(): void {
if (this.cachedUsernames.length <= this.MAX_CACHE_SIZE) return
public tryClearCache(): void {
if (this.cachedUsernames.length <= this.MAX_CACHE_SIZE) return
const needToRemove = this.cachedUsernames.length - this.MAX_CACHE_SIZE
const removed = this.cachedUsernames.splice(0, needToRemove)
console.log('Remove users from cache', needToRemove, removed)
}
const needToRemove = this.cachedUsernames.length - this.MAX_CACHE_SIZE
const removed = this.cachedUsernames.splice(0, needToRemove)
console.log('Remove users from cache', needToRemove, removed)
}
private addToCacheSingle(username: EnsuredUsername): void {
if (this.isInCache(username)) return
private addToCacheSingle(username: EnsuredUsername): void {
if (this.isInCache(username)) return
console.log('Add to cache', username, this.cachedUsernames.length)
this.cachedUsernames.push(username)
}
}
console.log('Add to cache', username, this.cachedUsernames.length)
this.cachedUsernames.push(username)
}
}

View File

@ -3,6 +3,7 @@ import { RedisClientType } from 'redis'
const KEY_FOR_TIMESTAMP = 'TIMESTAMP'
const KEY_FOR_COUNTER = 'COUNTER'
const KEY_FOR_PAYMENTS = 'PAYMENTS'
export class MetricsService {
private readonly registry: Registry
@ -32,13 +33,19 @@ export class MetricsService {
public updateLatestUsage(key: string): void {
const date = new Date()
this.db.hSet(KEY_FOR_TIMESTAMP, {
[key]: date.toLocaleString('ru-RU', {timeZone: 'Asia/Yekaterinburg'})
}).catch(console.error)
this.db
.hSet(KEY_FOR_TIMESTAMP, {
[key]: date.toLocaleString('ru-RU', { timeZone: 'Asia/Yekaterinburg' }),
})
.catch(console.error)
this.db.hIncrBy(KEY_FOR_COUNTER, key, 1).catch(console.error)
}
public updateLatestPaymentsCall(key: string): void {
this.db.hIncrBy(KEY_FOR_PAYMENTS, key, 1).catch(console.error)
}
public addReply(): void {
this.replyCounter.inc()
}

View File

@ -7,13 +7,13 @@ import { MetricsService } from './metrics.js'
describe('repository', () => {
let dbMock = mock<RedisClientType<any, any, any>>()
let metricsMock = mock<MetricsService>()
let cacheMock = mock<CacheService>()
let metricsMock = mock<MetricsService>()
let cacheMock = mock<CacheService>()
beforeEach(() => {
let dbMock = mock<RedisClientType<any, any, any>>()
let metricsMock = mock<MetricsService>()
let cacheMock = mock<CacheService>()
dbMock = mock<RedisClientType<any, any, any>>()
metricsMock = mock<MetricsService>()
cacheMock = mock<CacheService>()
})
describe('#addUsers', () => {

View File

@ -4,8 +4,6 @@ import { MetricsService } from './metrics.js'
import { CacheService } from './cache.js'
export class UserRepository {
constructor(
private readonly db: RedisClientType<any, any, any>,
private readonly metrics: MetricsService,
@ -18,8 +16,8 @@ export class UserRepository {
public async addUsers(chatId: Chat['id'], users: User[]): Promise<void> {
const usernamesById: Record<string, string> = {}
users.forEach((user) => {
if (!user.username || user.is_bot || this.cache.isInCache(user.username)) return
if (!user.username || user.is_bot || this.cache.isInCache(user.username))
return
this.cache.addToCache([user.username])
usernamesById[this.convertId(user.id)] = user.username
})
@ -40,7 +38,9 @@ export class UserRepository {
const chatIdStr = this.convertId(chatId)
try {
const promises = Object.entries(usernamesById).map(([id, username]) => this.db.hSet(chatIdStr, id, username))
const promises = Object.entries(usernamesById).map(([id, username]) =>
this.db.hSet(chatIdStr, id, username)
)
await Promise.all(promises)
} catch (err2) {

View File

@ -1,4 +1,3 @@
export const isChatGroup = (chatId: number): boolean => {
return chatId < 0
return chatId < 0
}

163
dist/core/bot.js vendored
View File

@ -6,10 +6,6 @@ export class Bot {
metricsService;
bot;
MENTION_COMMANDS = ['@all', '/all'];
FEEDBACK_COMMAND = '/feedback';
CODE_COMMAND = '/code';
SUPPORT_PAY_COMMAND = '/support';
PRIVACY_COMMAND = '/privacy';
ADMIN_ID;
isListening = false;
constructor(userRepository, metricsService, botName, adminId, token) {
@ -21,15 +17,34 @@ export class Bot {
this.MENTION_COMMANDS.push(botName);
this.ADMIN_ID = adminId;
this.bot = new Telegraf(token);
this.bot.on(message('text'), async (ctx) => {
const { message: { from, text, message_id }, chat: { id }, } = ctx;
await this.handleMessage({
from,
text,
messageId: message_id,
chatId: id,
}, (...args) => ctx.reply(...args));
});
this.bot.telegram.setMyCommands([
{
command: 'all',
description: 'Mention all users in a group',
},
{
command: 'donate',
description: 'Get crypto payments links for supporting the project',
},
{
command: 'feedback',
description: 'Send feedback or bug reports (English please)',
},
{
command: 'code',
description: 'Get link for bot opensource code',
},
{
command: 'privacy',
description: 'How the Bot takes care of your personal data',
},
]);
this.registerDonateCommand();
this.registerFeedbackCommand();
this.registerPrivacyCommand();
this.registerCodeCommand();
// Should be last for not overriding commands below
this.registerHandleMessage();
this.bot.on(message('new_chat_members'), ({ message, chat }) => this.handleAddMembers(chat.id, message.new_chat_members));
this.bot.on(message('left_chat_member'), ({ chat, message }) => this.handleDelete(chat.id, message.left_chat_member));
}
@ -42,24 +57,8 @@ export class Bot {
process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
this.bot.launch();
}
async handleMessage({ from, text, messageId, chatId }, reply) {
if (text.startsWith(this.PRIVACY_COMMAND)) {
console.log('Send privacy policy');
const message = `
Are you concerned about your security and personal data? This is right!
What do we use? Identifiers of your groups to store data about participants in them: usernames and identifiers to correctly call all users of the group.
All data is transmitted only via encrypted channels and is not used for other purposes.
We don't read your messages, don't log data about you in public systems and 3th party services except safe hosting and database.
You can view the project's codebase using ${this.CODE_COMMAND}.
Be careful when using unfamiliar bots in your communication, it can be dangerous!
`;
reply(message, {
reply_to_message_id: messageId,
parse_mode: 'HTML'
});
return;
}
if (text.startsWith(this.SUPPORT_PAY_COMMAND)) {
registerDonateCommand() {
this.bot.command('donate', (ctx) => {
console.log('Send payments info');
const message = `
This bot is free to use, but host and database are paid options for project.
@ -69,63 +68,89 @@ Support via USDT-TRX: <code>TJyEa6p3HvAHz34gn7hZHYNwY65iHryu3w</code>
Support via USDT-ETH: <code>0x7f49e01c13fE782aEB06Dc35a37d357b955b67B0</code>
Support via BTC: <code>bc1qgmq6033fnte2ata8ku3zgvj0n302zvr9cexcng</code>
Thank you for using and help!
Note, than you can send ${this.FEEDBACK_COMMAND} with features or problems.
Note, than you can send /feedback with features or problems.
`;
reply(message, {
reply_to_message_id: messageId,
parse_mode: 'HTML'
this.metricsService.updateLatestPaymentsCall(`${ctx.chat.id}`);
ctx.reply(message, {
reply_to_message_id: ctx.message.message_id,
parse_mode: 'HTML',
});
return;
}
if (text.startsWith(this.CODE_COMMAND)) {
console.log('Send code info');
reply(`I am an opensource project, feel free to reuse code or make bot better via /feedback.\nGithub link: https://github.com/sadfsdfdsa/allbot`, {
reply_to_message_id: messageId,
});
return;
}
if (text.startsWith(this.FEEDBACK_COMMAND)) {
const feedback = text.split(this.FEEDBACK_COMMAND)[1] || undefined;
});
}
registerFeedbackCommand() {
this.bot.command('feedback', (ctx) => {
const { message: { from, text, message_id: messageId }, chat: { id: chatId }, } = ctx;
console.log(ctx.message);
const feedback = text.split('/feedback')[1] || undefined;
if (!feedback) {
console.log(`Receive empty feedback from user ${from.username} in ${chatId}: ${feedback}`);
reply(`Add something in your feedback as feature or bug report`, {
ctx.reply(`Add something in your feedback as feature or bug report`, {
reply_to_message_id: messageId,
});
return;
}
console.log(`Receive feedback from user ${from.username} in ${chatId}: ${feedback}`);
reply(`Your review has been successfully registered, we will contact you, thank you!`, {
ctx.reply(`Your review has been successfully registered, we will contact you, thank you!`, {
reply_to_message_id: messageId,
});
if (!this.ADMIN_ID)
return;
this.bot.telegram.sendMessage(this.ADMIN_ID, `There is a new feedback from @${from.username} in chat group ${chatId}:\n${feedback}`);
return;
}
if (!isChatGroup(chatId)) {
console.log('Direct message from', from.username);
reply(`Add me to your group, here is example @all mention for you:`);
reply(`All from ${from.username}: @${from.username}`, {
});
}
registerPrivacyCommand() {
this.bot.command('privacy', (ctx) => {
console.log('Send privacy policy');
const message = `
Are you concerned about your security and personal data? This is right!
What do we use? Identifiers of your groups to store data about participants in them: usernames and identifiers to correctly call all users of the group.
All data is transmitted only via encrypted channels and is not used for other purposes.
We don't read your messages, don't log data about you in public systems and 3th party services except safe hosting and database.
You can view the project's codebase using /code.
Be careful when using unfamiliar bots in your communication, it can be dangerous!
`;
ctx.reply(message, {
reply_to_message_id: ctx.message.message_id,
parse_mode: 'HTML',
});
});
}
registerCodeCommand() {
this.bot.command('privacy', (ctx) => {
console.log('Send code info');
ctx.reply(`I am an opensource project, feel free to reuse code or make bot better via /feedback.\nGithub link: https://github.com/sadfsdfdsa/allbot`, {
reply_to_message_id: ctx.message.message_id,
});
});
}
registerHandleMessage() {
this.bot.on(message('text'), async (ctx) => {
const { message: { from, text, message_id: messageId }, chat: { id: chatId }, } = ctx;
if (!isChatGroup(chatId)) {
console.log('Direct message from', from.username);
ctx.reply(`Add me to your group, here is example @all mention for you:`);
ctx.reply(`All from ${from.username}: @${from.username}`, {
reply_to_message_id: messageId,
});
return;
}
await this.userRepository.addUsers(chatId, [from]);
const isCallAll = this.MENTION_COMMANDS.some((command) => text.includes(command));
if (!isCallAll)
return;
console.log(`Mention with pattern in group`, chatId);
const chatUsernames = await this.userRepository.getUsernamesByChatId(chatId);
if (!Object.values(chatUsernames).length)
return;
const str = Object.values(chatUsernames).map((username) => `@${username} `);
this.metricsService.addReply();
ctx.reply(`All from ${from.username}: ${str}`, {
reply_to_message_id: messageId,
});
return;
}
await this.userRepository.addUsers(chatId, [from]);
const isCallAll = this.MENTION_COMMANDS.some((command) => text.includes(command));
if (!isCallAll)
return;
console.log(`Mention with pattern in group`, chatId);
const chatUsernames = await this.userRepository.getUsernamesByChatId(chatId);
if (!Object.values(chatUsernames).length)
return;
const str = Object.values(chatUsernames).map((username) => `@${username} `);
this.metricsService.addReply();
reply(`All from ${from.username}: ${str}`, {
reply_to_message_id: messageId,
});
}
handleAddMembers(chatId, users) {
console.log('Try add new members', users.map(item => item.username));
console.log('Try add new members', users.map((item) => item.username));
return this.userRepository.addUsers(chatId, users);
}
handleDelete(chatId, user) {

12
dist/core/metrics.js vendored
View File

@ -1,6 +1,7 @@
import { Registry, Counter, collectDefaultMetrics } from 'prom-client';
const KEY_FOR_TIMESTAMP = 'TIMESTAMP';
const KEY_FOR_COUNTER = 'COUNTER';
const KEY_FOR_PAYMENTS = 'PAYMENTS';
export class MetricsService {
db;
registry;
@ -22,11 +23,16 @@ export class MetricsService {
}
updateLatestUsage(key) {
const date = new Date();
this.db.hSet(KEY_FOR_TIMESTAMP, {
[key]: date.toLocaleString('ru-RU', { timeZone: 'Asia/Yekaterinburg' })
}).catch(console.error);
this.db
.hSet(KEY_FOR_TIMESTAMP, {
[key]: date.toLocaleString('ru-RU', { timeZone: 'Asia/Yekaterinburg' }),
})
.catch(console.error);
this.db.hIncrBy(KEY_FOR_COUNTER, key, 1).catch(console.error);
}
updateLatestPaymentsCall(key) {
this.db.hIncrBy(KEY_FOR_PAYMENTS, key, 1).catch(console.error);
}
addReply() {
this.replyCounter.inc();
}