mirror of
https://github.com/sadfsdfdsa/allbot.git
synced 2024-11-19 00:31:42 +02:00
feat: prometheus and express for healthchecking
This commit is contained in:
parent
e702e1c5f0
commit
88965d4f47
@ -1,3 +1,4 @@
|
||||
REDIS_URI=rediss://your-host-uri
|
||||
TG_TOKEN=STRING_FROM_BOT_FATHER
|
||||
BOT_NAME=@allsuperior_bot
|
||||
BOT_NAME=@allsuperior_bot
|
||||
SERVER_PORT=8080
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
.env
|
||||
.env.test
|
||||
.env.prod
|
||||
node_modules
|
@ -11,4 +11,5 @@ It's require `.env` after fork:
|
||||
REDIS_URI=rediss://your-host-uri
|
||||
TG_TOKEN=STRING_FROM_BOT_FATHER
|
||||
BOT_NAME=@allsuperior_bot
|
||||
SERVER_PORT=8080
|
||||
```
|
||||
|
@ -1,14 +0,0 @@
|
||||
import { Telegraf } from 'telegraf'
|
||||
import { createBot } from './bot.js'
|
||||
|
||||
describe('bot', () => {
|
||||
describe('#createBot', () => {
|
||||
test('should throw error if not token passed', () => {
|
||||
expect(() => createBot(undefined)).toThrow()
|
||||
})
|
||||
|
||||
test('should return instance of bot', () => {
|
||||
expect(createBot('123') instanceof Telegraf).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
111
core/bot.ts
111
core/bot.ts
@ -1,12 +1,103 @@
|
||||
import { Telegraf } from 'telegraf'
|
||||
import { Context, 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'
|
||||
|
||||
export const createBot = (token?: string) => {
|
||||
console.log('Starting bot')
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No tg token set')
|
||||
}
|
||||
const bot = new Telegraf(token)
|
||||
|
||||
return bot
|
||||
type HandleMessagePayload = {
|
||||
chatId: Chat['id']
|
||||
text: string
|
||||
from: NonNullable<Message['from']>
|
||||
messageId: Message['message_id']
|
||||
}
|
||||
|
||||
export class Bot {
|
||||
private bot: Telegraf
|
||||
|
||||
private readonly COMMANDS = ['@all', '/all']
|
||||
|
||||
private isListening = false
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly metricsService: MetricsService,
|
||||
botName: string | undefined,
|
||||
token: string | undefined
|
||||
) {
|
||||
if (!token) throw new Error('No tg token set')
|
||||
|
||||
if (botName) this.COMMANDS.push(botName)
|
||||
|
||||
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: Parameters<Context['sendMessage']>) => ctx.reply(...args)
|
||||
)
|
||||
})
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
public launch(): void {
|
||||
if (this.isListening) throw new Error('Bot already listening')
|
||||
|
||||
console.log('Bot starting')
|
||||
|
||||
this.isListening = true
|
||||
|
||||
process.once('SIGINT', () => this.bot.stop('SIGINT'))
|
||||
process.once('SIGTERM', () => this.bot.stop('SIGTERM'))
|
||||
|
||||
this.bot.launch()
|
||||
}
|
||||
|
||||
private async handleMessage(
|
||||
{ from, text, messageId, chatId }: HandleMessagePayload,
|
||||
reply: Context['reply']
|
||||
): Promise<void> {
|
||||
await this.userRepository.addUsers(chatId, [from])
|
||||
|
||||
const isCallAll = this.COMMANDS.some((command) => text.includes(command))
|
||||
console.log(`Message, should reply=${isCallAll}`, text)
|
||||
if (!isCallAll) return
|
||||
|
||||
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)
|
||||
return this.userRepository.addUsers(chatId, users)
|
||||
}
|
||||
|
||||
private handleDelete(chatId: Chat['id'], user: User): Promise<void> {
|
||||
console.log('Delete user', user.username)
|
||||
return this.userRepository.deleteUser(chatId, user.id)
|
||||
}
|
||||
}
|
||||
|
38
core/metrics.ts
Normal file
38
core/metrics.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { Registry, Counter, collectDefaultMetrics } from 'prom-client'
|
||||
|
||||
export class MetricsService {
|
||||
private readonly registry: Registry
|
||||
|
||||
private readonly replyCounter: Counter
|
||||
|
||||
constructor(measureDefaultMetrics = true) {
|
||||
this.registry = new Registry()
|
||||
|
||||
this.replyCounter = new Counter({
|
||||
name: 'allbot_replies_counter',
|
||||
help: 'The number of total replies of bot',
|
||||
})
|
||||
this.registry.registerMetric(this.replyCounter)
|
||||
|
||||
if (!measureDefaultMetrics) return
|
||||
collectDefaultMetrics({
|
||||
register: this.registry,
|
||||
})
|
||||
}
|
||||
|
||||
public addReply(): void {
|
||||
this.replyCounter.inc()
|
||||
}
|
||||
|
||||
public async getMetrics(): Promise<{
|
||||
contentType: string
|
||||
metrics: string
|
||||
}> {
|
||||
const metrics = await this.registry.metrics()
|
||||
|
||||
return {
|
||||
metrics,
|
||||
contentType: this.registry.contentType,
|
||||
}
|
||||
}
|
||||
}
|
41
core/server.ts
Normal file
41
core/server.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import { MetricsService } from './metrics.js'
|
||||
|
||||
export class Server {
|
||||
private app: ReturnType<typeof express>
|
||||
|
||||
private isListening = false
|
||||
|
||||
constructor(
|
||||
private readonly metrics: MetricsService,
|
||||
private readonly port: string | undefined
|
||||
) {
|
||||
if (!port) throw new Error('Incorrect port')
|
||||
|
||||
this.app = express()
|
||||
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.send({
|
||||
status: 200,
|
||||
})
|
||||
})
|
||||
|
||||
this.app.get('/metrics', async (req, res) => {
|
||||
const { contentType, metrics: metricsString } =
|
||||
await this.metrics.getMetrics()
|
||||
|
||||
res.setHeader('Content-Type', contentType)
|
||||
res.send(metricsString)
|
||||
})
|
||||
}
|
||||
|
||||
public listen(): void {
|
||||
if (this.isListening) throw new Error('Double listen')
|
||||
|
||||
this.isListening = true
|
||||
|
||||
this.app.listen(Number(this.port), () => {
|
||||
console.log(`Server is listening on port ${this.port}`)
|
||||
})
|
||||
}
|
||||
}
|
66
dist/core/bot.js
vendored
66
dist/core/bot.js
vendored
@ -1,9 +1,61 @@
|
||||
import { Telegraf } from 'telegraf';
|
||||
export const createBot = (token) => {
|
||||
console.log('Starting bot');
|
||||
if (!token) {
|
||||
throw new Error('No tg token set');
|
||||
import { message } from 'telegraf/filters';
|
||||
export class Bot {
|
||||
userRepository;
|
||||
metricsService;
|
||||
bot;
|
||||
COMMANDS = ['@all', '/all'];
|
||||
isListening = false;
|
||||
constructor(userRepository, metricsService, botName, token) {
|
||||
this.userRepository = userRepository;
|
||||
this.metricsService = metricsService;
|
||||
if (!token)
|
||||
throw new Error('No tg token set');
|
||||
if (botName)
|
||||
this.COMMANDS.push(botName);
|
||||
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.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));
|
||||
}
|
||||
const bot = new Telegraf(token);
|
||||
return bot;
|
||||
};
|
||||
launch() {
|
||||
if (this.isListening)
|
||||
throw new Error('Bot already listening');
|
||||
console.log('Bot starting');
|
||||
this.isListening = true;
|
||||
process.once('SIGINT', () => this.bot.stop('SIGINT'));
|
||||
process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
|
||||
this.bot.launch();
|
||||
}
|
||||
async handleMessage({ from, text, messageId, chatId }, reply) {
|
||||
await this.userRepository.addUsers(chatId, [from]);
|
||||
const isCallAll = this.COMMANDS.some((command) => text.includes(command));
|
||||
console.log(`Message, should reply=${isCallAll}`, text);
|
||||
if (!isCallAll)
|
||||
return;
|
||||
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);
|
||||
return this.userRepository.addUsers(chatId, users);
|
||||
}
|
||||
handleDelete(chatId, user) {
|
||||
console.log('Delete user', user.username);
|
||||
return this.userRepository.deleteUser(chatId, user.id);
|
||||
}
|
||||
}
|
||||
|
28
dist/core/metrics.js
vendored
Normal file
28
dist/core/metrics.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
import { Registry, Counter, collectDefaultMetrics } from 'prom-client';
|
||||
export class MetricsService {
|
||||
registry;
|
||||
replyCounter;
|
||||
constructor(measureDefaultMetrics = true) {
|
||||
this.registry = new Registry();
|
||||
this.replyCounter = new Counter({
|
||||
name: 'allbot_replies_counter',
|
||||
help: 'The number of total replies of bot',
|
||||
});
|
||||
this.registry.registerMetric(this.replyCounter);
|
||||
if (!measureDefaultMetrics)
|
||||
return;
|
||||
collectDefaultMetrics({
|
||||
register: this.registry,
|
||||
});
|
||||
}
|
||||
addReply() {
|
||||
this.replyCounter.inc();
|
||||
}
|
||||
async getMetrics() {
|
||||
const metrics = await this.registry.metrics();
|
||||
return {
|
||||
metrics,
|
||||
contentType: this.registry.contentType,
|
||||
};
|
||||
}
|
||||
}
|
32
dist/core/server.js
vendored
Normal file
32
dist/core/server.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import express from 'express';
|
||||
export class Server {
|
||||
metrics;
|
||||
port;
|
||||
app;
|
||||
isListening = false;
|
||||
constructor(metrics, port) {
|
||||
this.metrics = metrics;
|
||||
this.port = port;
|
||||
if (!port)
|
||||
throw new Error('Incorrect port');
|
||||
this.app = express();
|
||||
}
|
||||
listen() {
|
||||
if (this.isListening)
|
||||
throw new Error('Double listen');
|
||||
this.isListening = true;
|
||||
this.app.get('/health', (req, res) => {
|
||||
res.send({
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
this.app.get('/metrics', async (req, res) => {
|
||||
const { contentType, metrics: metricsString } = await this.metrics.getMetrics();
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.send(metricsString);
|
||||
});
|
||||
this.app.listen(Number(this.port), () => {
|
||||
console.log(`Server is listening on port ${this.port}`);
|
||||
});
|
||||
}
|
||||
}
|
44
dist/index.js
vendored
44
dist/index.js
vendored
@ -1,42 +1,16 @@
|
||||
import { message } from 'telegraf/filters';
|
||||
import 'dotenv/config';
|
||||
import { createDB } from './core/db.js';
|
||||
import { UserRepository } from './core/repository.js';
|
||||
import { createBot } from './core/bot.js';
|
||||
const NAME = process.env.BOT_NAME;
|
||||
const ALL_COMMANDS = ['@all', '/all'];
|
||||
if (NAME)
|
||||
ALL_COMMANDS.push(NAME);
|
||||
import { Bot } from './core/bot.js';
|
||||
import { MetricsService } from './core/metrics.js';
|
||||
import { Server } from './core/server.js';
|
||||
const main = async () => {
|
||||
const client = await createDB(process.env.REDIS_URI);
|
||||
const repository = new UserRepository(client);
|
||||
const bot = createBot(process.env.TG_TOKEN);
|
||||
bot.on(message('text'), async (ctx) => {
|
||||
const { message: { from, text, message_id }, chat: { id }, } = ctx;
|
||||
await repository.addUsers(id, [from]);
|
||||
const isCallAll = ALL_COMMANDS.some((command) => text.includes(command));
|
||||
console.log(`Message, should reply=${isCallAll}`, text);
|
||||
if (!isCallAll)
|
||||
return;
|
||||
const chatUsernames = await repository.getUsernamesByChatId(id);
|
||||
if (!Object.values(chatUsernames).length)
|
||||
return;
|
||||
const str = Object.values(chatUsernames).map((username) => `@${username} `);
|
||||
ctx.reply(`All from ${from.username}: ${str}`, {
|
||||
reply_to_message_id: message_id,
|
||||
});
|
||||
});
|
||||
bot.on(message('new_chat_members'), async ({ message, chat: { id } }) => {
|
||||
console.log('Try add new members', message.new_chat_members);
|
||||
await repository.addUsers(id, message.new_chat_members);
|
||||
});
|
||||
bot.on(message('left_chat_member'), async (ctx) => {
|
||||
console.log('Delete user', ctx.message.left_chat_member.username);
|
||||
await client.hDel(`${ctx.chat.id}`, `${ctx.message.left_chat_member.id}`);
|
||||
});
|
||||
const metricsService = new MetricsService();
|
||||
const server = new Server(metricsService, process.env.SERVER_PORT);
|
||||
const dbClient = await createDB(process.env.REDIS_URI);
|
||||
const userRepository = new UserRepository(dbClient);
|
||||
const bot = new Bot(userRepository, metricsService, process.env.BOT_NAME, process.env.TG_TOKEN);
|
||||
bot.launch();
|
||||
// Enable graceful stop
|
||||
process.once('SIGINT', () => bot.stop('SIGINT'));
|
||||
process.once('SIGTERM', () => bot.stop('SIGTERM'));
|
||||
server.listen();
|
||||
};
|
||||
main();
|
||||
|
55
index.ts
55
index.ts
@ -1,55 +1,28 @@
|
||||
import { message } from 'telegraf/filters'
|
||||
import 'dotenv/config'
|
||||
import { createDB } from './core/db.js'
|
||||
import { UserRepository } from './core/repository.js'
|
||||
import { createBot } from './core/bot.js'
|
||||
|
||||
const NAME = process.env.BOT_NAME
|
||||
const ALL_COMMANDS = ['@all', '/all']
|
||||
if (NAME) ALL_COMMANDS.push(NAME)
|
||||
import { Bot } from './core/bot.js'
|
||||
import { MetricsService } from './core/metrics.js'
|
||||
import { Server } from './core/server.js'
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const client = await createDB(process.env.REDIS_URI)
|
||||
const repository = new UserRepository(client)
|
||||
const bot = createBot(process.env.TG_TOKEN)
|
||||
const metricsService = new MetricsService()
|
||||
|
||||
bot.on(message('text'), async (ctx) => {
|
||||
const {
|
||||
message: { from, text, message_id },
|
||||
chat: { id },
|
||||
} = ctx
|
||||
const server = new Server(metricsService, process.env.SERVER_PORT)
|
||||
|
||||
await repository.addUsers(id, [from])
|
||||
const dbClient = await createDB(process.env.REDIS_URI)
|
||||
|
||||
const isCallAll = ALL_COMMANDS.some((command) => text.includes(command))
|
||||
console.log(`Message, should reply=${isCallAll}`, text)
|
||||
if (!isCallAll) return
|
||||
const userRepository = new UserRepository(dbClient)
|
||||
|
||||
const chatUsernames = await repository.getUsernamesByChatId(id)
|
||||
if (!Object.values(chatUsernames).length) return
|
||||
|
||||
const str = Object.values(chatUsernames).map((username) => `@${username} `)
|
||||
|
||||
ctx.reply(`All from ${from.username}: ${str}`, {
|
||||
reply_to_message_id: message_id,
|
||||
})
|
||||
})
|
||||
|
||||
bot.on(message('new_chat_members'), async ({ message, chat: { id } }) => {
|
||||
console.log('Try add new members', message.new_chat_members)
|
||||
await repository.addUsers(id, message.new_chat_members)
|
||||
})
|
||||
|
||||
bot.on(message('left_chat_member'), async (ctx) => {
|
||||
console.log('Delete user', ctx.message.left_chat_member.username)
|
||||
await client.hDel(`${ctx.chat.id}`, `${ctx.message.left_chat_member.id}`)
|
||||
})
|
||||
const bot = new Bot(
|
||||
userRepository,
|
||||
metricsService,
|
||||
process.env.BOT_NAME,
|
||||
process.env.TG_TOKEN
|
||||
)
|
||||
|
||||
bot.launch()
|
||||
|
||||
// Enable graceful stop
|
||||
process.once('SIGINT', () => bot.stop('SIGINT'))
|
||||
process.once('SIGTERM', () => bot.stop('SIGTERM'))
|
||||
server.listen()
|
||||
}
|
||||
|
||||
main()
|
||||
|
728
package-lock.json
generated
728
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
||||
"author": "Artem Shuvaev <shuvaevlol@gmail.com>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/node": "^20.5.3",
|
||||
"jest": "^29.6.4",
|
||||
@ -24,6 +25,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"prom-client": "^14.2.0",
|
||||
"redis": "^4.6.8",
|
||||
"telegraf": "^4.12.2"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user