Building an MVP Feedback Bot for Max.ru Using Long Polling and SQLite
Max.ru provides developers with access to its Bot API for building interactive tools. For this MVP feedback bot, we use Node.js with the @maxhub/max-bot-api library and SQLite. Long Polling enables local development without requiring webhooks, servers, or HTTPS.
The key challenge is identifying the client when the admin replies. Reply messages include a link with the original message's mid, allowing us to map responses back to users via the database.
Choosing the Tech Stack and Setting Up the Project
The @maxhub/max-bot-api library supports Long Polling out of the box, eliminating the need for ngrok or Cloudflare Tunnel during development.
SQLite is ideal for MVP: it’s a file-based database with no separate server, minimal latency, and consistent request handling.
Install dependencies:
npm init -y
npm install @maxhub/max-bot-api sqlite sqlite3 dotenv
Create a .env file:
BOT_TOKEN=your_token_here
OWNER_ID=12345678
The token is generated in business.max.ru, and OWNER_ID is your personal Max user ID.
Initializing SQLite and Database Structure
Use async connection and create the reply_map table for mapping:
import { open } from 'sqlite';
import sqlite3 from 'sqlite3';
let db;
(async () => {
db = await open({ filename: './database.sqlite', driver: sqlite3.Database });
await db.exec(`
CREATE TABLE IF NOT EXISTS reply_map (
owner_msg_mid TEXT PRIMARY KEY,
client_user_id INTEGER
)
`);
console.log('Database ready');
})();
This table stores the mapping from owner_msg_mid to client_user_id.
Handling Messages from Clients
The message_created event checks the sender. Client messages are forwarded to the admin with Markdown formatting:
const ownerId = Number(process.env.OWNER_ID);
bot.on('message_created', async (ctx) => {
const msg = ctx.message;
const senderId = msg.sender.user_id;
const text = msg.body.text;
if (senderId !== ownerId) {
const forwardText = `📩 **Message from ${msg.sender.first_name}** (ID: ${senderId}):
${text}`;
const sentMsg = await bot.api.sendMessageToUser(ownerId, forwardText, { format: 'markdown' });
if (sentMsg && sentMsg.body && sentMsg.body.mid) {
await db.run('INSERT OR REPLACE INTO reply_map (owner_msg_mid, client_user_id) VALUES (?, ?)',
[sentMsg.body.mid, senderId]);
}
return;
}
// reply logic
});
The mid of the sent message is stored in reply_map for later routing.
Admin Response Logic
Reply messages contain link.type = 'reply' and link.message.mid — the original message ID. To find the client:
- If
link.type === 'reply': extractrepliedMsgMid, query the DB. - Send the response back to the client using
client_user_id. - Confirm receipt to the admin.
Sample Reply JSON structure:
{
"body": { "text": "answer", ... },
"sender": { ... },
"link": {
"type": "reply",
"message": {
"mid": "mid.original_message..."
}
}
}
Benefits of This Architecture
- Local Development: Run with
npm start—no ports or SSL setup needed. - Reply Interface: Admins use the standard Reply function seamlessly.
- Simple Database: One file (
database.sqlite) for easy backups. - Scalability: Easy migration to PostgreSQL as traffic grows.
Key Takeaways
- Long Polling simplifies MVP development by removing the need for webhooks.
- The SQLite
reply_mapensures accurate response routing. - Markdown in forwarded messages adds context (client name, ID).
- Fallback for admin replies: last active client is used if no match found.
— Editorial Team
No comments yet.