Initial import
This commit is contained in:
15
apps/bot/AGENTS.md
Normal file
15
apps/bot/AGENTS.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Scope
|
||||
Applies within `apps/bot`.
|
||||
|
||||
## Responsibilities
|
||||
- Telegram admin bot runtime
|
||||
- allowlist checks
|
||||
- alert delivery
|
||||
- low-friction admin commands
|
||||
|
||||
## Rules
|
||||
- Pairing approval must never happen inside the bot runtime itself.
|
||||
- The bot may initiate pending pairing, but only the server-side CLI completes it.
|
||||
- Every command that changes state must produce an audit log entry.
|
||||
3
apps/bot/README.md
Normal file
3
apps/bot/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# apps/bot
|
||||
|
||||
Planned Telegram admin bot runtime. MVP should use long polling unless deployment requirements change.
|
||||
15
apps/bot/package.json
Normal file
15
apps/bot/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@nproxy/bot",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nproxy/config": "workspace:*",
|
||||
"@nproxy/db": "workspace:*",
|
||||
"@nproxy/providers": "workspace:*"
|
||||
}
|
||||
}
|
||||
0
apps/bot/src/.gitkeep
Normal file
0
apps/bot/src/.gitkeep
Normal file
100
apps/bot/src/main.ts
Normal file
100
apps/bot/src/main.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { loadConfig } from "@nproxy/config";
|
||||
import { createPrismaTelegramBotStore, prisma } from "@nproxy/db";
|
||||
import { createTelegramBotApiTransport, type TelegramUpdate } from "@nproxy/providers";
|
||||
|
||||
const config = loadConfig();
|
||||
const telegramStore = createPrismaTelegramBotStore(prisma);
|
||||
const telegramTransport = createTelegramBotApiTransport(config.telegram.botToken);
|
||||
const pairingExpiresInMinutes = 15;
|
||||
let nextUpdateOffset: number | undefined;
|
||||
let isPolling = false;
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
service: "bot",
|
||||
mode: config.telegram.botMode,
|
||||
}),
|
||||
);
|
||||
|
||||
void pollLoop();
|
||||
|
||||
process.once("SIGTERM", async () => {
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.once("SIGINT", async () => {
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
async function pollLoop(): Promise<void> {
|
||||
if (isPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPolling = true;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const updates = await telegramTransport.getUpdates({
|
||||
...(nextUpdateOffset !== undefined ? { offset: nextUpdateOffset } : {}),
|
||||
timeoutSeconds: 25,
|
||||
});
|
||||
|
||||
for (const update of updates) {
|
||||
await handleUpdate(update);
|
||||
nextUpdateOffset = update.update_id + 1;
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
console.log("bot polling heartbeat");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("bot polling failed", error);
|
||||
setTimeout(() => {
|
||||
isPolling = false;
|
||||
void pollLoop();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate(update: TelegramUpdate): Promise<void> {
|
||||
const message = update.message;
|
||||
const from = message?.from;
|
||||
|
||||
if (!message || !from) {
|
||||
return;
|
||||
}
|
||||
|
||||
const telegramUserId = String(from.id);
|
||||
const displayNameSnapshot = [from.first_name, from.last_name].filter(Boolean).join(" ");
|
||||
const isAllowed = await telegramStore.isTelegramAdminAllowed(telegramUserId);
|
||||
|
||||
if (isAllowed) {
|
||||
await telegramTransport.sendMessage({
|
||||
chatId: message.chat.id,
|
||||
text: "Admin access is active.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const challenge = await telegramStore.getOrCreatePendingPairingChallenge(
|
||||
{
|
||||
telegramUserId,
|
||||
...(from.username ? { telegramUsername: from.username } : {}),
|
||||
displayNameSnapshot: displayNameSnapshot || "Telegram user",
|
||||
},
|
||||
pairingExpiresInMinutes,
|
||||
);
|
||||
|
||||
await telegramTransport.sendMessage({
|
||||
chatId: message.chat.id,
|
||||
text: [
|
||||
`Your pairing code is: ${challenge.code}`,
|
||||
`Run nproxy pair ${challenge.code} on the server.`,
|
||||
`Expires at ${challenge.expiresAt.toISOString()}.`,
|
||||
].join("\n"),
|
||||
});
|
||||
}
|
||||
12
apps/bot/tsconfig.json
Normal file
12
apps/bot/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user