Skip to content

Commit bbb14a7

Browse files
committed
feat: add Telegram webhook router for app and template moderation commands
1 parent 06a25f6 commit bbb14a7

3 files changed

Lines changed: 525 additions & 0 deletions

File tree

backend/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createAppsRouter } from './routes/apps'
66
import { createUsersRouter } from './routes/users'
77
import { createTemplatesRouter } from './routes/templates'
88
import { createAiRouter } from './routes/ai'
9+
import { createTelegramRouter } from './routes/telegram'
910
import { createNotificationService } from './services/notification'
1011
import cors from 'cors'
1112
import path from 'path'
@@ -55,6 +56,7 @@ app.use('/api', createAppsRouter(db, notificationService))
5556
app.use('/api', createUsersRouter(db))
5657
app.use('/api/templates', createTemplatesRouter(db, notificationService))
5758
app.use('/api/ai', createAiRouter(db))
59+
app.use('/api/telegram', createTelegramRouter(db))
5860

5961
const port = process.env.PORT || 3001
6062
app.listen(port, () => {

backend/src/routes/telegram.ts

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { Router, Request, Response, RequestHandler } from 'express'
2+
import { Knex } from 'knex'
3+
import * as dotenv from 'dotenv'
4+
import path from 'path'
5+
6+
// Type definition for telegram update
7+
interface TelegramUpdate {
8+
update_id: number
9+
message?: {
10+
message_id: number
11+
from: {
12+
id: number
13+
is_bot: boolean
14+
first_name: string
15+
username?: string
16+
}
17+
chat: {
18+
id: number
19+
first_name: string
20+
username?: string
21+
type: string
22+
}
23+
date: number
24+
text?: string
25+
}
26+
}
27+
28+
/**
29+
* Creates and configures the telegram webhook router
30+
* @param {Knex} db - The database connection instance
31+
* @returns {Router} Express router configured with telegram webhook route
32+
*/
33+
export function createTelegramRouter(db: Knex): Router {
34+
const router = Router()
35+
36+
// Load environment variables
37+
const isDist = __dirname.includes('dist')
38+
const envPath = isDist ? path.resolve(__dirname, '../../../.env') : path.resolve(__dirname, '../../.env')
39+
dotenv.config({ path: envPath })
40+
41+
// Webhook endpoint for Telegram
42+
router.post('/webhook', (async (req: Request, res: Response) => {
43+
try {
44+
const update = req.body as TelegramUpdate
45+
46+
// Check if the message exists
47+
if (!update.message || !update.message.text) {
48+
return res.sendStatus(200) // Acknowledge the request but do nothing
49+
}
50+
51+
const chatId = update.message.chat.id
52+
const message = update.message.text.trim()
53+
const authorizedChatId = process.env.TELEGRAM_CHAT_ID
54+
55+
// Check if the chat is authorized
56+
if (!authorizedChatId || chatId.toString() !== authorizedChatId) {
57+
return res.status(200).json({
58+
method: 'sendMessage',
59+
chat_id: chatId,
60+
text: 'You have no access to this bot.',
61+
})
62+
}
63+
64+
// Process the message
65+
const lowerCaseMessage = message.toLowerCase()
66+
67+
// Command for making apps public (moderated = true)
68+
if (lowerCaseMessage.startsWith('public apps:')) {
69+
const idsText = message.substring('public apps:'.length).trim()
70+
const ids = parseIds(idsText)
71+
72+
if (ids.length === 0) {
73+
return res.status(200).json({
74+
method: 'sendMessage',
75+
chat_id: chatId,
76+
text: 'No valid app IDs provided. Format should be "public apps: 1, 2, 3"',
77+
})
78+
}
79+
80+
try {
81+
// Update apps to be moderated
82+
await db('apps').whereIn('id', ids).update({ moderated: true })
83+
84+
// Get count of updated apps
85+
const updatedCount = await db('apps')
86+
.whereIn('id', ids)
87+
.where('moderated', true)
88+
.count({ count: 'id' })
89+
.first()
90+
91+
return res.status(200).json({
92+
method: 'sendMessage',
93+
chat_id: chatId,
94+
text: `Successfully made ${updatedCount?.count || 0} app(s) public`,
95+
})
96+
} catch (error) {
97+
console.error('Error updating app moderation status:', error)
98+
return res.status(200).json({
99+
method: 'sendMessage',
100+
chat_id: chatId,
101+
text: 'Error updating app moderation status',
102+
})
103+
}
104+
}
105+
106+
// Command for making templates public (moderated = true)
107+
if (lowerCaseMessage.startsWith('public templates:')) {
108+
const idsText = message.substring('public templates:'.length).trim()
109+
const ids = parseIds(idsText)
110+
111+
if (ids.length === 0) {
112+
return res.status(200).json({
113+
method: 'sendMessage',
114+
chat_id: chatId,
115+
text: 'No valid template IDs provided. Format should be "public templates: 1, 2, 3"',
116+
})
117+
}
118+
119+
try {
120+
// Update templates to be moderated
121+
await db('templates').whereIn('id', ids).update({ moderated: true })
122+
123+
// Get count of updated templates
124+
const updatedCount = await db('templates')
125+
.whereIn('id', ids)
126+
.where('moderated', true)
127+
.count({ count: 'id' })
128+
.first()
129+
130+
return res.status(200).json({
131+
method: 'sendMessage',
132+
chat_id: chatId,
133+
text: `Successfully made ${updatedCount?.count || 0} template(s) public`,
134+
})
135+
} catch (error) {
136+
console.error('Error updating template moderation status:', error)
137+
return res.status(200).json({
138+
method: 'sendMessage',
139+
chat_id: chatId,
140+
text: 'Error updating template moderation status',
141+
})
142+
}
143+
}
144+
145+
// Command for making apps private (moderated = false)
146+
if (lowerCaseMessage.startsWith('private apps:')) {
147+
const idsText = message.substring('private apps:'.length).trim()
148+
const ids = parseIds(idsText)
149+
150+
if (ids.length === 0) {
151+
return res.status(200).json({
152+
method: 'sendMessage',
153+
chat_id: chatId,
154+
text: 'No valid app IDs provided. Format should be "private apps: 1, 2, 3"',
155+
})
156+
}
157+
158+
try {
159+
// Update apps to be unmoderated
160+
await db('apps').whereIn('id', ids).update({ moderated: false })
161+
162+
// Get count of updated apps
163+
const updatedCount = await db('apps')
164+
.whereIn('id', ids)
165+
.where('moderated', false)
166+
.count({ count: 'id' })
167+
.first()
168+
169+
return res.status(200).json({
170+
method: 'sendMessage',
171+
chat_id: chatId,
172+
text: `Successfully made ${updatedCount?.count || 0} app(s) private`,
173+
})
174+
} catch (error) {
175+
console.error('Error updating app moderation status:', error)
176+
return res.status(200).json({
177+
method: 'sendMessage',
178+
chat_id: chatId,
179+
text: 'Error updating app moderation status',
180+
})
181+
}
182+
}
183+
184+
// Command for making templates private (moderated = false)
185+
if (lowerCaseMessage.startsWith('private templates:')) {
186+
const idsText = message.substring('private templates:'.length).trim()
187+
const ids = parseIds(idsText)
188+
189+
if (ids.length === 0) {
190+
return res.status(200).json({
191+
method: 'sendMessage',
192+
chat_id: chatId,
193+
text: 'No valid template IDs provided. Format should be "private templates: 1, 2, 3"',
194+
})
195+
}
196+
197+
try {
198+
// Update templates to be unmoderated
199+
await db('templates').whereIn('id', ids).update({ moderated: false })
200+
201+
// Get count of updated templates
202+
const updatedCount = await db('templates')
203+
.whereIn('id', ids)
204+
.where('moderated', false)
205+
.count({ count: 'id' })
206+
.first()
207+
208+
return res.status(200).json({
209+
method: 'sendMessage',
210+
chat_id: chatId,
211+
text: `Successfully made ${updatedCount?.count || 0} template(s) private`,
212+
})
213+
} catch (error) {
214+
console.error('Error updating template moderation status:', error)
215+
return res.status(200).json({
216+
method: 'sendMessage',
217+
chat_id: chatId,
218+
text: 'Error updating template moderation status',
219+
})
220+
}
221+
}
222+
223+
// If none of the commands matched, send help message
224+
return res.status(200).json({
225+
method: 'sendMessage',
226+
chat_id: chatId,
227+
text:
228+
'Supported commands:\n' +
229+
'- "public apps: 1, 2, 3" - Make specified apps public\n' +
230+
'- "public templates: 1, 2, 3" - Make specified templates public\n' +
231+
'- "private apps: 1, 2, 3" - Make specified apps private\n' +
232+
'- "private templates: 1, 2, 3" - Make specified templates private',
233+
})
234+
} catch (error) {
235+
console.error('Error processing telegram webhook:', error)
236+
res.sendStatus(200) // Always respond with 200 to Telegram
237+
}
238+
}) as RequestHandler)
239+
240+
return router
241+
}
242+
243+
/**
244+
* Parse a comma-separated string of IDs into an array of numbers
245+
* @param {string} text - The comma-separated string of IDs
246+
* @returns {number[]} Array of valid numeric IDs
247+
*/
248+
function parseIds(text: string): number[] {
249+
if (!text) return []
250+
251+
return text
252+
.split(',')
253+
.map(id => id.trim())
254+
.filter(id => id && !isNaN(Number(id)))
255+
.map(id => Number(id))
256+
}

0 commit comments

Comments
 (0)