Telegram Syndication¶
Overview¶
The marketplace automatically syndicates listings to Telegram channels using an event-driven architecture. When a listing is created or updated, it's posted to the configured Telegram channel.
Architecture¶
┌─────────────────┐
│ Listing Created/ │
│ Updated │
└────────┬────────┘
│
▼
┌─────────────────┐
│ listingEmitter │ (EventEmitter singleton)
│ (core/) │
└────────┬────────┘
│
▼
┌─────────────────────┐
│ telegramListener │ (Event listener)
│ (syndication/) │
└────────┬────────────┘
│
├── On update: Try edit existing message
│
└── Create new post
│
▼
┌─────────────────────┐
│ TelegramBotService │ (API integration)
│ (syndication/) │
└────────┬────────────┘
│
├── 1 image → sendPhoto()
└── 2+ images → sendMediaGroup()
│
▼
Telegram API
Implementation¶
Event Emitter¶
File: backend/src/core/listingEmitter.js:1-7
import { EventEmitter } from 'events';
const listingEmitter = new EventEmitter();
listingEmitter.setMaxListeners(20);
export default listingEmitter;
Event Listener¶
File: backend/src/modules/syndication/listeners/telegramListener.js:125-143
export function initializeListingListeners() {
if (listenersInitialized) return;
listingEmitter.on('listing:created', (listingId) => {
handleSyndication(listingId, 'listing:created');
});
listingEmitter.on('listing:updated', (listingId) => {
handleSyndication(listingId, 'listing:updated');
});
listenersInitialized = true;
}
Initialized in: backend/src/server.js:12
Syndication Handler¶
File: backend/src/modules/syndication/listeners/telegramListener.js:7-123
Flow for listing:updated:
- Fetch listing with category and agent
- Check for existing successful syndication log with
messageId - If found, attempt
editMessageCaption() - If edit fails (message deleted, etc.), fall through to create new post
- For
listing:createdor no existing message, create new post - Log result to
SyndicationLogtable
Telegram Bot Service¶
File: backend/src/modules/syndication/services/telegramBot.service.js
Configuration¶
// Lines 10-15
async function getTelegramConfig() {
const config = await prisma.syndicationConfig.findUnique({
where: { platform: 'TELEGRAM' },
});
return config;
}
Caption Generation¶
File: telegramBot.service.js:54-104
function buildCaption(listing) {
// Custom caption override
if (listing.customTelegramCaption && listing.customTelegramCaption.trim()) {
return listing.customTelegramCaption.trim();
}
// Auto-generated caption
const sections = [
`*${title}*`,
'',
`💰 *Price:* ${price}`,
`📍 *Location:* ${location}`,
`🏷 *Category:* ${category}`,
`📋 *Status:* ${status}`,
];
// Add dynamic attributes
if (attrLines.length > 0) {
sections.push('', '*Details:*');
sections.push(...attrLines);
}
// Add agent info
sections.push(
'',
'─────────────────',
`👤 *Agent:* ${agentName}`,
`📞 *Phone:* ${phone}`,
'',
'Listed on _BROS Technology_',
);
return sections.join('\n');
}
Image Handling¶
File: telegramBot.service.js:158-247
- Single image:
sendPhoto()with caption - Multiple images:
sendMediaGroup()(max 10 images) - Images read from local filesystem or fetched from URLs
- Converted to FormData blobs for Telegram API
// Lines 186-247
async function sendMediaGroup(caption, imagePaths, telegramApi, channelId) {
const validPaths = imagePaths.slice(0, MAX_IMAGES_IN_GROUP); // Max 10
// ... read images, build FormData, send to Telegram API
}
Message Operations¶
// Edit message caption (line 342-357)
static async editMessageCaption(messageId, newCaption) {
await axios.post(`${telegramApi}/editMessageCaption`, {
chat_id: channelId,
message_id: messageId,
caption: newCaption,
parse_mode: 'Markdown',
});
}
// Delete message (line 330-340)
static async deleteMessage(messageId) {
await axios.post(`${telegramApi}/deleteMessage`, {
chat_id: channelId,
message_id: messageId,
});
}
Syndication Log¶
Every syndication attempt is logged to SyndicationLog table:
| Field | Description |
|---|---|
listingId |
Related listing |
platform |
"TELEGRAM" |
status |
PENDING, SUCCESS, FAILED |
action |
NEW_POST, EDITED, DELETED |
channelInfo |
Channel ID |
messageId |
Telegram message_id (for edits) |
errorMessage |
Error details if failed |
runAt |
Execution timestamp |
API Endpoints¶
Configuration¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/syndication/config |
🔒 | List all configs |
GET |
/api/syndication/config/:platform |
🔒 | Get platform config |
POST |
/api/syndication/config |
🔒 (Admin) | Create/update config |
DELETE |
/api/syndication/config/:platform |
🔒 (Admin) | Delete config |
Telegram Operations¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/syndication/telegram/info |
🔒 | Get bot + channel info |
POST |
/api/syndication/delete-message/:messageId |
🔒 (Admin) | Delete Telegram message |
POST |
/api/syndication/edit-message/:messageId |
🔒 (Admin) | Edit message caption |
Logs¶
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/syndication/logs |
🔒 | List logs (paginated) |
POST |
/api/syndication/retry/:id |
🔒 | Retry failed syndication |
Retry Logic¶
File: syndication.routes.js:183-251
Failed syndications can be retried:
router.post('/retry/:id', authenticate(), async (req, res) => {
// 1. Find existing log
// 2. Verify ownership (AGENT can only retry own)
// 3. Re-emit listing:created event
// 4. Update log status to PENDING
});
Configuration¶
Bot Token¶
Get from @BotFather on Telegram.
Channel ID¶
- Add bot to your channel as admin
- Send a message to the channel
- Use
https://api.telegram.org/bot<TOKEN>/getUpdatesto find the channel ID
Setup via API¶
curl -X POST http://localhost:5000/api/syndication/config \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"platform": "TELEGRAM",
"botToken": "123456:ABC-DEF...",
"channelId": "@your_channel",
"isActive": true
}'
Constants¶
File: telegramBot.service.js:6-8