mikrotik-telegram 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/dist/actions.d.ts +38 -0
- package/dist/actions.js +40 -0
- package/dist/alerts.d.ts +50 -0
- package/dist/alerts.js +103 -0
- package/dist/audit.d.ts +20 -0
- package/dist/audit.js +44 -0
- package/dist/automation.d.ts +57 -0
- package/dist/automation.js +70 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +64 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +34 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/mikrotik-service.d.ts +27 -0
- package/dist/mikrotik-service.js +225 -0
- package/dist/rest-client.d.ts +24 -0
- package/dist/rest-client.js +55 -0
- package/dist/runtime-adapters.d.ts +56 -0
- package/dist/runtime-adapters.js +87 -0
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +46 -0
- package/dist/telegram-client.d.ts +19 -0
- package/dist/telegram-client.js +29 -0
- package/dist/telegram-commands.d.ts +26 -0
- package/dist/telegram-commands.js +76 -0
- package/dist/types.d.ts +209 -0
- package/dist/types.js +1 -0
- package/package.json +61 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const PackageConfigSchema: z.ZodObject<{
|
|
3
|
+
mikrotik: z.ZodObject<{
|
|
4
|
+
host: z.ZodString;
|
|
5
|
+
username: z.ZodString;
|
|
6
|
+
password: z.ZodString;
|
|
7
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
8
|
+
https: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
telegram: z.ZodObject<{
|
|
12
|
+
token: z.ZodString;
|
|
13
|
+
allowedChatIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
14
|
+
webhookSecret: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
security: z.ZodDefault<z.ZodObject<{
|
|
17
|
+
confirmationTtlSeconds: z.ZodDefault<z.ZodNumber>;
|
|
18
|
+
defaultRole: z.ZodDefault<z.ZodEnum<{
|
|
19
|
+
admin: "admin";
|
|
20
|
+
operator: "operator";
|
|
21
|
+
viewer: "viewer";
|
|
22
|
+
}>>;
|
|
23
|
+
adminChatIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
24
|
+
operatorChatIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
25
|
+
}, z.core.$strip>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
export type PackageConfig = z.infer<typeof PackageConfigSchema>;
|
|
28
|
+
export declare function createConfig(input: unknown): PackageConfig;
|
|
29
|
+
export declare function createConfigFromEnv(env: Record<string, string | undefined>): PackageConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const BooleanSchema = z.preprocess((value) => {
|
|
3
|
+
if (typeof value === 'boolean')
|
|
4
|
+
return value;
|
|
5
|
+
if (typeof value === 'string')
|
|
6
|
+
return value.toLowerCase() === 'true' || value === '1';
|
|
7
|
+
return value;
|
|
8
|
+
}, z.boolean());
|
|
9
|
+
export const PackageConfigSchema = z.object({
|
|
10
|
+
mikrotik: z.object({
|
|
11
|
+
host: z.string().min(1),
|
|
12
|
+
username: z.string().min(1),
|
|
13
|
+
password: z.string().min(1),
|
|
14
|
+
port: z.number().int().min(1).max(65535).default(80),
|
|
15
|
+
https: z.boolean().default(false),
|
|
16
|
+
timeoutMs: z.number().int().positive().default(10_000),
|
|
17
|
+
}),
|
|
18
|
+
telegram: z.object({
|
|
19
|
+
token: z.string().min(1),
|
|
20
|
+
allowedChatIds: z.array(z.string()).default([]),
|
|
21
|
+
webhookSecret: z.string().min(16).optional(),
|
|
22
|
+
}),
|
|
23
|
+
security: z.object({
|
|
24
|
+
confirmationTtlSeconds: z.number().int().positive().default(60),
|
|
25
|
+
defaultRole: z.enum(['admin', 'operator', 'viewer']).default('viewer'),
|
|
26
|
+
adminChatIds: z.array(z.string()).default([]),
|
|
27
|
+
operatorChatIds: z.array(z.string()).default([]),
|
|
28
|
+
}).default({
|
|
29
|
+
confirmationTtlSeconds: 60,
|
|
30
|
+
defaultRole: 'viewer',
|
|
31
|
+
adminChatIds: [],
|
|
32
|
+
operatorChatIds: [],
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
export function createConfig(input) {
|
|
36
|
+
return PackageConfigSchema.parse(input);
|
|
37
|
+
}
|
|
38
|
+
export function createConfigFromEnv(env) {
|
|
39
|
+
const allowedChatIds = (env.TELEGRAM_ALLOWED_CHAT_IDS || env.TELEGRAM_CHAT_ID || '')
|
|
40
|
+
.split(',').map((value) => value.trim()).filter(Boolean);
|
|
41
|
+
const adminChatIds = (env.TELEGRAM_ADMIN_CHAT_IDS || '').split(',').map((value) => value.trim()).filter(Boolean);
|
|
42
|
+
const operatorChatIds = (env.TELEGRAM_OPERATOR_CHAT_IDS || '').split(',').map((value) => value.trim()).filter(Boolean);
|
|
43
|
+
return createConfig({
|
|
44
|
+
mikrotik: {
|
|
45
|
+
host: env.MIKROTIK_HOST,
|
|
46
|
+
username: env.MIKROTIK_USER,
|
|
47
|
+
password: env.MIKROTIK_PASSWORD,
|
|
48
|
+
port: Number(env.MIKROTIK_HTTP_PORT || 80),
|
|
49
|
+
https: BooleanSchema.parse(env.MIKROTIK_HTTPS || false),
|
|
50
|
+
timeoutMs: Number(env.MIKROTIK_TIMEOUT_MS || 10_000),
|
|
51
|
+
},
|
|
52
|
+
telegram: {
|
|
53
|
+
token: env.TELEGRAM_BOT_TOKEN,
|
|
54
|
+
allowedChatIds,
|
|
55
|
+
webhookSecret: env.TELEGRAM_WEBHOOK_SECRET,
|
|
56
|
+
},
|
|
57
|
+
security: {
|
|
58
|
+
confirmationTtlSeconds: Number(env.TELEGRAM_CONFIRMATION_TTL_SECONDS || 60),
|
|
59
|
+
defaultRole: 'viewer',
|
|
60
|
+
adminChatIds,
|
|
61
|
+
operatorChatIds,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class NetPulseError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly details?: unknown | undefined;
|
|
4
|
+
constructor(message: string, code: string, details?: unknown | undefined);
|
|
5
|
+
}
|
|
6
|
+
export declare class RouterOsHttpError extends NetPulseError {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
readonly endpoint: string;
|
|
9
|
+
constructor(status: number, message: string, endpoint: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class AuthorizationError extends NetPulseError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class ConfirmationRequiredError extends NetPulseError {
|
|
15
|
+
readonly confirmationId: string;
|
|
16
|
+
constructor(confirmationId: string, message?: string);
|
|
17
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class NetPulseError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, code, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = 'NetPulseError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class RouterOsHttpError extends NetPulseError {
|
|
12
|
+
status;
|
|
13
|
+
endpoint;
|
|
14
|
+
constructor(status, message, endpoint, details) {
|
|
15
|
+
super(message, 'ROUTEROS_HTTP_ERROR', details);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.endpoint = endpoint;
|
|
18
|
+
this.name = 'RouterOsHttpError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class AuthorizationError extends NetPulseError {
|
|
22
|
+
constructor(message = 'Commande non autorisée.') {
|
|
23
|
+
super(message, 'TELEGRAM_UNAUTHORIZED');
|
|
24
|
+
this.name = 'AuthorizationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ConfirmationRequiredError extends NetPulseError {
|
|
28
|
+
confirmationId;
|
|
29
|
+
constructor(confirmationId, message = 'Confirmation requise.') {
|
|
30
|
+
super(message, 'CONFIRMATION_REQUIRED');
|
|
31
|
+
this.confirmationId = confirmationId;
|
|
32
|
+
this.name = 'ConfirmationRequiredError';
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './config';
|
|
2
|
+
export * from './errors';
|
|
3
|
+
export * from './rest-client';
|
|
4
|
+
export * from './mikrotik-service';
|
|
5
|
+
export * from './telegram-client';
|
|
6
|
+
export * from './telegram-commands';
|
|
7
|
+
export * from './runtime';
|
|
8
|
+
export * from './runtime-adapters';
|
|
9
|
+
export * from './alerts';
|
|
10
|
+
export * from './automation';
|
|
11
|
+
export * from './actions';
|
|
12
|
+
export * from './types';
|
|
13
|
+
export * from './audit';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './config';
|
|
2
|
+
export * from './errors';
|
|
3
|
+
export * from './rest-client';
|
|
4
|
+
export * from './mikrotik-service';
|
|
5
|
+
export * from './telegram-client';
|
|
6
|
+
export * from './telegram-commands';
|
|
7
|
+
export * from './runtime';
|
|
8
|
+
export * from './runtime-adapters';
|
|
9
|
+
export * from './alerts';
|
|
10
|
+
export * from './automation';
|
|
11
|
+
export * from './actions';
|
|
12
|
+
export * from './types';
|
|
13
|
+
export * from './audit';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { RouterOsRestClient } from './rest-client';
|
|
2
|
+
import type { ActiveSession, CreateHotspotUserInput, CreateProfileInput, CreateSchedulerInput, CreateScriptInput, HotspotProfile, HotspotUser, MutationResult, RouterId, RouterScheduler, RouterScript, SystemHealth, UpdateHotspotUserInput, UpdateProfileInput, UpdateSchedulerInput, UserQuery, GenerateVouchersInput, GenerateVouchersResult, BanUserInput, BanResult, UnbanUserInput, DeleteUserInput, DisconnectUserInput } from './types';
|
|
3
|
+
export declare class MikroTikService {
|
|
4
|
+
private readonly client;
|
|
5
|
+
constructor(client: RouterOsRestClient);
|
|
6
|
+
getSystemHealth(): Promise<SystemHealth>;
|
|
7
|
+
listHotspotUsers(query?: UserQuery): Promise<HotspotUser[]>;
|
|
8
|
+
createHotspotUser(input: CreateHotspotUserInput): Promise<HotspotUser>;
|
|
9
|
+
generateVouchers(input: GenerateVouchersInput): Promise<GenerateVouchersResult>;
|
|
10
|
+
updateHotspotUser(id: RouterId, input: UpdateHotspotUserInput): Promise<HotspotUser>;
|
|
11
|
+
deleteHotspotUser(id: RouterId): Promise<MutationResult>;
|
|
12
|
+
listProfiles(): Promise<HotspotProfile[]>;
|
|
13
|
+
createProfile(input: CreateProfileInput): Promise<HotspotProfile>;
|
|
14
|
+
updateProfile(id: RouterId, input: UpdateProfileInput): Promise<HotspotProfile>;
|
|
15
|
+
deleteProfile(id: RouterId): Promise<MutationResult>;
|
|
16
|
+
listActiveSessions(): Promise<ActiveSession[]>;
|
|
17
|
+
disconnectSession(id: RouterId): Promise<MutationResult>;
|
|
18
|
+
banUser(input: BanUserInput): Promise<BanResult>;
|
|
19
|
+
unbanUser(input: UnbanUserInput): Promise<MutationResult>;
|
|
20
|
+
deleteUser(input: DeleteUserInput): Promise<MutationResult>;
|
|
21
|
+
disconnectUser(input: DisconnectUserInput): Promise<MutationResult>;
|
|
22
|
+
createScript(input: CreateScriptInput): Promise<RouterScript>;
|
|
23
|
+
runScript(id: RouterId): Promise<MutationResult>;
|
|
24
|
+
createScheduler(input: CreateSchedulerInput): Promise<RouterScheduler>;
|
|
25
|
+
updateScheduler(id: RouterId, input: UpdateSchedulerInput): Promise<RouterScheduler>;
|
|
26
|
+
deleteScheduler(id: RouterId): Promise<MutationResult>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const RouterIdSchema = z.string().min(1);
|
|
3
|
+
const UserSchema = z.object({
|
|
4
|
+
name: z.string().min(1).max(64), password: z.string().min(1).max(128), profile: z.string().min(1),
|
|
5
|
+
comment: z.string().max(255).optional(), limitUptime: z.string().min(1).optional(),
|
|
6
|
+
});
|
|
7
|
+
const ProfileSchema = z.object({
|
|
8
|
+
name: z.string().min(1).max(64), rateLimit: z.string().max(128).optional(),
|
|
9
|
+
sharedUsers: z.number().int().positive().optional(), onLogin: z.string().max(4096).optional(),
|
|
10
|
+
});
|
|
11
|
+
const ScriptSchema = z.object({ name: z.string().min(1).max(64), source: z.string().min(1), comment: z.string().max(255).optional() });
|
|
12
|
+
const SchedulerSchema = z.object({
|
|
13
|
+
name: z.string().min(1).max(64), interval: z.string().min(1), onEvent: z.string().min(1),
|
|
14
|
+
comment: z.string().max(255).optional(), disabled: z.boolean().optional(),
|
|
15
|
+
});
|
|
16
|
+
const VoucherGenerationSchema = z.object({
|
|
17
|
+
count: z.number().int().min(1).max(500), length: z.number().int().min(4).max(32).default(6),
|
|
18
|
+
prefix: z.string().regex(/^[A-Za-z0-9_-]*$/).max(16).default('NET'), profile: z.string().min(1).default('default'),
|
|
19
|
+
passwordLength: z.number().int().min(4).max(32).default(8), limitUptime: z.string().min(1).optional(),
|
|
20
|
+
comment: z.string().max(255).optional(), price: z.number().nonnegative().optional(), expiresAt: z.string().datetime().optional(),
|
|
21
|
+
dryRun: z.boolean().default(false), duplicateCheck: z.boolean().default(true),
|
|
22
|
+
});
|
|
23
|
+
const IpAddressSchema = z.string().trim().refine((value) => /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(value), 'Adresse IP invalide');
|
|
24
|
+
const BanSchema = z.object({
|
|
25
|
+
address: IpAddressSchema.optional(), user: z.string().min(1).optional(), comment: z.string().max(255).optional(), timeout: z.string().min(1).default('1d'),
|
|
26
|
+
}).refine((value) => Boolean(value.address || value.user), 'address ou user est requis');
|
|
27
|
+
const UnbanSchema = z.object({ ruleId: z.string().min(1).optional(), address: IpAddressSchema.optional() }).refine((value) => Boolean(value.ruleId || value.address), 'ruleId ou address est requis');
|
|
28
|
+
function idOf(record) {
|
|
29
|
+
const id = record['.id'] || record.ret || record.id;
|
|
30
|
+
return RouterIdSchema.parse(id);
|
|
31
|
+
}
|
|
32
|
+
function userOf(record) {
|
|
33
|
+
return { id: idOf(record), name: record.name || '', password: record.password, profile: record.profile, comment: record.comment, limitUptime: record['limit-uptime'], uptime: record.uptime };
|
|
34
|
+
}
|
|
35
|
+
function randomCode(length) {
|
|
36
|
+
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
37
|
+
return Array.from({ length }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join('');
|
|
38
|
+
}
|
|
39
|
+
export class MikroTikService {
|
|
40
|
+
client;
|
|
41
|
+
constructor(client) {
|
|
42
|
+
this.client = client;
|
|
43
|
+
}
|
|
44
|
+
async getSystemHealth() {
|
|
45
|
+
const started = Date.now();
|
|
46
|
+
const [resource, identity, board] = await Promise.all([
|
|
47
|
+
this.client.get('/system/resource'),
|
|
48
|
+
this.client.list('/system/identity'),
|
|
49
|
+
this.client.list('/system/routerboard'),
|
|
50
|
+
]);
|
|
51
|
+
const metrics = resource;
|
|
52
|
+
return {
|
|
53
|
+
identity: identity[0]?.name || 'Unknown', model: metrics['board-name'] || board[0]?.model || 'Unknown',
|
|
54
|
+
version: metrics.version || 'Unknown', uptime: metrics.uptime || 'Unknown',
|
|
55
|
+
cpuLoadPercent: Number(metrics['cpu-load'] || 0), totalMemoryBytes: Number(metrics['total-memory'] || 0),
|
|
56
|
+
freeMemoryBytes: Number(metrics['free-memory'] || 0), totalStorageBytes: Number(metrics['total-hdd-space'] || 0),
|
|
57
|
+
freeStorageBytes: Number(metrics['free-hdd-space'] || 0), latencyMs: Date.now() - started,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async listHotspotUsers(query = {}) {
|
|
61
|
+
const params = new URLSearchParams();
|
|
62
|
+
if (query.name)
|
|
63
|
+
params.set('name', query.name);
|
|
64
|
+
if (query.profile)
|
|
65
|
+
params.set('profile', query.profile);
|
|
66
|
+
const records = await this.client.list(`/ip/hotspot/user${params.size ? `?${params}` : ''}`);
|
|
67
|
+
return records.slice(0, query.limit || records.length).map(userOf);
|
|
68
|
+
}
|
|
69
|
+
async createHotspotUser(input) {
|
|
70
|
+
const data = UserSchema.parse({ profile: 'default', ...input });
|
|
71
|
+
const record = await this.client.create('/ip/hotspot/user/add', {
|
|
72
|
+
name: data.name, password: data.password, profile: data.profile,
|
|
73
|
+
...(data.comment ? { comment: data.comment } : {}), ...(data.limitUptime ? { 'limit-uptime': data.limitUptime } : {}),
|
|
74
|
+
});
|
|
75
|
+
const id = idOf(record);
|
|
76
|
+
return userOf({ ...record, '.id': id, name: data.name, password: data.password, profile: data.profile });
|
|
77
|
+
}
|
|
78
|
+
async generateVouchers(input) {
|
|
79
|
+
const data = VoucherGenerationSchema.parse(input);
|
|
80
|
+
const existingNames = data.duplicateCheck ? new Set((await this.listHotspotUsers()).map((user) => user.name)) : new Set();
|
|
81
|
+
const vouchers = [];
|
|
82
|
+
let duplicateSkipped = 0;
|
|
83
|
+
for (let index = 0; index < data.count; index += 1) {
|
|
84
|
+
let candidate;
|
|
85
|
+
do {
|
|
86
|
+
candidate = `${data.prefix}-${randomCode(data.length)}-${index + 1}`;
|
|
87
|
+
} while (data.duplicateCheck && existingNames.has(candidate));
|
|
88
|
+
if (data.duplicateCheck && existingNames.has(candidate)) {
|
|
89
|
+
duplicateSkipped += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
existingNames.add(candidate);
|
|
93
|
+
vouchers.push({
|
|
94
|
+
name: candidate,
|
|
95
|
+
password: randomCode(data.passwordLength),
|
|
96
|
+
profile: data.profile,
|
|
97
|
+
price: data.price,
|
|
98
|
+
expiresAt: data.expiresAt,
|
|
99
|
+
created: false,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (data.dryRun) {
|
|
103
|
+
return { requested: data.count, created: 0, dryRun: true, duplicateSkipped, vouchers: vouchers.map((voucher) => ({ ...voucher, created: false })) };
|
|
104
|
+
}
|
|
105
|
+
const created = await Promise.all(vouchers.map(async (voucher) => {
|
|
106
|
+
const metadata = [data.comment, data.price ? `price=${data.price}` : undefined, data.expiresAt ? `expires=${data.expiresAt}` : undefined].filter(Boolean).join(' | ');
|
|
107
|
+
await this.createHotspotUser({
|
|
108
|
+
name: voucher.name,
|
|
109
|
+
password: voucher.password,
|
|
110
|
+
profile: voucher.profile,
|
|
111
|
+
limitUptime: data.limitUptime,
|
|
112
|
+
comment: metadata || data.comment,
|
|
113
|
+
});
|
|
114
|
+
return { ...voucher, created: true };
|
|
115
|
+
}));
|
|
116
|
+
return { requested: data.count, created: created.length, dryRun: false, duplicateSkipped, vouchers: created };
|
|
117
|
+
}
|
|
118
|
+
async updateHotspotUser(id, input) {
|
|
119
|
+
const validId = RouterIdSchema.parse(id);
|
|
120
|
+
const data = UserSchema.partial().parse(input);
|
|
121
|
+
await this.client.update('/ip/hotspot/user', validId, {
|
|
122
|
+
...(data.name ? { name: data.name } : {}), ...(data.password ? { password: data.password } : {}),
|
|
123
|
+
...(data.profile ? { profile: data.profile } : {}), ...(data.comment !== undefined ? { comment: data.comment } : {}),
|
|
124
|
+
...(data.limitUptime ? { 'limit-uptime': data.limitUptime } : {}),
|
|
125
|
+
});
|
|
126
|
+
const updated = await this.client.get(`/ip/hotspot/user/${encodeURIComponent(validId)}`);
|
|
127
|
+
return userOf({ ...updated, '.id': validId });
|
|
128
|
+
}
|
|
129
|
+
async deleteHotspotUser(id) {
|
|
130
|
+
const validId = RouterIdSchema.parse(id);
|
|
131
|
+
await this.client.remove('/ip/hotspot/user', validId);
|
|
132
|
+
return { success: true, id: validId, operation: 'delete-hotspot-user' };
|
|
133
|
+
}
|
|
134
|
+
async listProfiles() {
|
|
135
|
+
const records = await this.client.list('/ip/hotspot/user/profile');
|
|
136
|
+
return records.map((record) => ({ id: idOf(record), name: record.name || '', rateLimit: record['rate-limit'], sharedUsers: Number(record['shared-users'] || 0), onLogin: record['on-login'] }));
|
|
137
|
+
}
|
|
138
|
+
async createProfile(input) {
|
|
139
|
+
const data = ProfileSchema.parse(input);
|
|
140
|
+
const record = await this.client.create('/ip/hotspot/user/profile/add', {
|
|
141
|
+
name: data.name, ...(data.rateLimit ? { 'rate-limit': data.rateLimit } : {}),
|
|
142
|
+
...(data.sharedUsers ? { 'shared-users': data.sharedUsers } : {}), ...(data.onLogin ? { 'on-login': data.onLogin } : {}),
|
|
143
|
+
});
|
|
144
|
+
return { id: idOf(record), name: data.name, rateLimit: data.rateLimit, sharedUsers: data.sharedUsers, onLogin: data.onLogin };
|
|
145
|
+
}
|
|
146
|
+
async updateProfile(id, input) {
|
|
147
|
+
const validId = RouterIdSchema.parse(id);
|
|
148
|
+
const data = ProfileSchema.partial().parse(input);
|
|
149
|
+
const record = await this.client.update('/ip/hotspot/user/profile', validId, {
|
|
150
|
+
...(data.name ? { name: data.name } : {}), ...(data.rateLimit ? { 'rate-limit': data.rateLimit } : {}),
|
|
151
|
+
...(data.sharedUsers ? { 'shared-users': data.sharedUsers } : {}), ...(data.onLogin ? { 'on-login': data.onLogin } : {}),
|
|
152
|
+
});
|
|
153
|
+
return { id: validId, name: record.name || data.name || '', rateLimit: record['rate-limit'] || data.rateLimit, sharedUsers: Number(record['shared-users'] || data.sharedUsers || 0), onLogin: record['on-login'] || data.onLogin };
|
|
154
|
+
}
|
|
155
|
+
async deleteProfile(id) {
|
|
156
|
+
const validId = RouterIdSchema.parse(id);
|
|
157
|
+
await this.client.remove('/ip/hotspot/user/profile', validId);
|
|
158
|
+
return { success: true, id: validId, operation: 'delete-profile' };
|
|
159
|
+
}
|
|
160
|
+
async listActiveSessions() {
|
|
161
|
+
const records = await this.client.list('/ip/hotspot/active');
|
|
162
|
+
return records.map((record) => ({ id: idOf(record), user: record.user || '', address: record.address, macAddress: record['mac-address'], uptime: record.uptime, sessionTimeLeft: record['session-time-left'], bytesIn: Number(record['bytes-in'] || 0), bytesOut: Number(record['bytes-out'] || 0) }));
|
|
163
|
+
}
|
|
164
|
+
async disconnectSession(id) {
|
|
165
|
+
const validId = RouterIdSchema.parse(id);
|
|
166
|
+
await this.client.remove('/ip/hotspot/active', validId);
|
|
167
|
+
return { success: true, id: validId, operation: 'disconnect-session' };
|
|
168
|
+
}
|
|
169
|
+
async banUser(input) {
|
|
170
|
+
const data = BanSchema.parse(input);
|
|
171
|
+
const comment = data.comment || `NetPulse ban${data.user ? ` user=${data.user}` : ''}`;
|
|
172
|
+
const record = await this.client.create('/ip/firewall/address-list/add', {
|
|
173
|
+
list: 'netpulse-ban', address: data.address || data.user, timeout: data.timeout, comment,
|
|
174
|
+
});
|
|
175
|
+
const ruleId = idOf(record);
|
|
176
|
+
return { success: true, operation: 'ban-user', address: data.address, ruleId };
|
|
177
|
+
}
|
|
178
|
+
async unbanUser(input) {
|
|
179
|
+
const data = UnbanSchema.parse(input);
|
|
180
|
+
const ruleId = data.ruleId || (await this.client.findOne('/ip/firewall/address-list', 'address', data.address || ''))?.['.id'];
|
|
181
|
+
if (!ruleId)
|
|
182
|
+
return { success: false, operation: 'unban-user' };
|
|
183
|
+
await this.client.remove('/ip/firewall/address-list', ruleId);
|
|
184
|
+
return { success: true, id: ruleId, operation: 'unban-user' };
|
|
185
|
+
}
|
|
186
|
+
async deleteUser(input) {
|
|
187
|
+
const id = RouterIdSchema.parse(input.id);
|
|
188
|
+
return this.deleteHotspotUser(id);
|
|
189
|
+
}
|
|
190
|
+
async disconnectUser(input) {
|
|
191
|
+
const sessionId = RouterIdSchema.parse(input.sessionId);
|
|
192
|
+
return this.disconnectSession(sessionId);
|
|
193
|
+
}
|
|
194
|
+
async createScript(input) {
|
|
195
|
+
const data = ScriptSchema.parse(input);
|
|
196
|
+
const record = await this.client.create('/system/script/add', data);
|
|
197
|
+
return { id: idOf(record), name: data.name, source: data.source, comment: data.comment };
|
|
198
|
+
}
|
|
199
|
+
async runScript(id) {
|
|
200
|
+
const validId = RouterIdSchema.parse(id);
|
|
201
|
+
await this.client.request('POST', '/system/script/run', { '.id': validId });
|
|
202
|
+
return { success: true, id: validId, operation: 'run-script' };
|
|
203
|
+
}
|
|
204
|
+
async createScheduler(input) {
|
|
205
|
+
const data = SchedulerSchema.parse(input);
|
|
206
|
+
const record = await this.client.create('/system/scheduler/add', {
|
|
207
|
+
name: data.name, interval: data.interval, 'on-event': data.onEvent, disabled: data.disabled ? 'yes' : 'no', ...(data.comment ? { comment: data.comment } : {}),
|
|
208
|
+
});
|
|
209
|
+
return { id: idOf(record), name: data.name, interval: data.interval, onEvent: data.onEvent, disabled: Boolean(data.disabled) };
|
|
210
|
+
}
|
|
211
|
+
async updateScheduler(id, input) {
|
|
212
|
+
const validId = RouterIdSchema.parse(id);
|
|
213
|
+
const data = SchedulerSchema.partial().parse(input);
|
|
214
|
+
const record = await this.client.update('/system/scheduler', validId, {
|
|
215
|
+
...(data.name ? { name: data.name } : {}), ...(data.interval ? { interval: data.interval } : {}), ...(data.onEvent ? { 'on-event': data.onEvent } : {}),
|
|
216
|
+
...(data.comment !== undefined ? { comment: data.comment } : {}), ...(data.disabled !== undefined ? { disabled: data.disabled ? 'yes' : 'no' } : {}),
|
|
217
|
+
});
|
|
218
|
+
return { id: validId, name: record.name || data.name || '', interval: record.interval || data.interval || '', onEvent: record['on-event'] || data.onEvent || '', disabled: record.disabled === 'true' || record.disabled === 'yes' };
|
|
219
|
+
}
|
|
220
|
+
async deleteScheduler(id) {
|
|
221
|
+
const validId = RouterIdSchema.parse(id);
|
|
222
|
+
await this.client.remove('/system/scheduler', validId);
|
|
223
|
+
return { success: true, id: validId, operation: 'delete-scheduler' };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RouterPayload, RouterRecord, RouterResponse } from './types';
|
|
2
|
+
export interface RestClientOptions {
|
|
3
|
+
readonly host: string;
|
|
4
|
+
readonly port: number;
|
|
5
|
+
readonly username: string;
|
|
6
|
+
readonly password: string;
|
|
7
|
+
readonly https?: boolean;
|
|
8
|
+
readonly timeoutMs?: number;
|
|
9
|
+
readonly fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
export declare class RouterOsRestClient {
|
|
12
|
+
private readonly baseUrl;
|
|
13
|
+
private readonly authorization;
|
|
14
|
+
private readonly requestTimeoutMs;
|
|
15
|
+
private readonly fetcher;
|
|
16
|
+
constructor(options: RestClientOptions);
|
|
17
|
+
request<T extends RouterResponse = RouterResponse>(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', endpoint: string, body?: RouterPayload): Promise<T>;
|
|
18
|
+
list<T extends RouterRecord = RouterRecord>(endpoint: string): Promise<T[]>;
|
|
19
|
+
get<T extends RouterRecord = RouterRecord>(endpoint: string): Promise<T>;
|
|
20
|
+
create<T extends RouterRecord = RouterRecord>(endpoint: string, body: RouterPayload): Promise<T>;
|
|
21
|
+
update<T extends RouterRecord = RouterRecord>(endpoint: string, id: string, body: RouterPayload): Promise<T>;
|
|
22
|
+
remove(endpoint: string, id: string): Promise<RouterResponse>;
|
|
23
|
+
findOne<T extends RouterRecord = RouterRecord>(endpoint: string, field: string, value: string): Promise<T | undefined>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { RouterOsHttpError } from './errors';
|
|
2
|
+
export class RouterOsRestClient {
|
|
3
|
+
baseUrl;
|
|
4
|
+
authorization;
|
|
5
|
+
requestTimeoutMs;
|
|
6
|
+
fetcher;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
const scheme = options.https ? 'https' : 'http';
|
|
9
|
+
this.baseUrl = `${scheme}://${options.host}:${options.port}/rest`;
|
|
10
|
+
this.authorization = `Basic ${btoa(`${options.username}:${options.password}`)}`;
|
|
11
|
+
this.requestTimeoutMs = options.timeoutMs ?? 10_000;
|
|
12
|
+
this.fetcher = options.fetch ?? fetch;
|
|
13
|
+
}
|
|
14
|
+
async request(method, endpoint, body) {
|
|
15
|
+
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
16
|
+
const response = await this.fetcher(`${this.baseUrl}${normalizedEndpoint}`, {
|
|
17
|
+
method,
|
|
18
|
+
headers: { Authorization: this.authorization, 'Content-Type': 'application/json' },
|
|
19
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
20
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
21
|
+
});
|
|
22
|
+
const text = await response.text();
|
|
23
|
+
let parsed = text;
|
|
24
|
+
try {
|
|
25
|
+
parsed = text ? JSON.parse(text) : null;
|
|
26
|
+
}
|
|
27
|
+
catch { /* RouterOS may return plain text. */ }
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const message = response.status === 401
|
|
30
|
+
? 'Authentification REST RouterOS refusée.'
|
|
31
|
+
: `Erreur REST RouterOS HTTP ${response.status}.`;
|
|
32
|
+
throw new RouterOsHttpError(response.status, message, normalizedEndpoint, parsed);
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
list(endpoint) {
|
|
37
|
+
return this.request('GET', endpoint);
|
|
38
|
+
}
|
|
39
|
+
get(endpoint) {
|
|
40
|
+
return this.request('GET', endpoint);
|
|
41
|
+
}
|
|
42
|
+
create(endpoint, body) {
|
|
43
|
+
return this.request('POST', endpoint, body);
|
|
44
|
+
}
|
|
45
|
+
update(endpoint, id, body) {
|
|
46
|
+
return this.request('PATCH', `${endpoint}/${encodeURIComponent(id)}`, body);
|
|
47
|
+
}
|
|
48
|
+
remove(endpoint, id) {
|
|
49
|
+
return this.request('DELETE', `${endpoint}/${encodeURIComponent(id)}`);
|
|
50
|
+
}
|
|
51
|
+
async findOne(endpoint, field, value) {
|
|
52
|
+
const query = `${encodeURIComponent(field)}=${encodeURIComponent(value)}`;
|
|
53
|
+
return (await this.list(`${endpoint}?${query}`))[0];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TelegramUpdate } from './types';
|
|
2
|
+
export interface RuntimeAdapter {
|
|
3
|
+
readonly fetch: typeof fetch;
|
|
4
|
+
now(): Date;
|
|
5
|
+
sleep(milliseconds: number): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export interface BotRuntimeConfig {
|
|
8
|
+
readonly token: string;
|
|
9
|
+
readonly allowedChatIds?: readonly string[];
|
|
10
|
+
readonly adminChatIds?: readonly string[];
|
|
11
|
+
readonly operatorChatIds?: readonly string[];
|
|
12
|
+
readonly webhookSecret?: string;
|
|
13
|
+
readonly onUpdate?: (update: TelegramUpdate, runtime: RuntimeAdapter) => Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createRuntimeAdapter(fetcher?: typeof fetch): RuntimeAdapter;
|
|
16
|
+
export interface UpdateHandler {
|
|
17
|
+
(update: TelegramUpdate): Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface WebhookHandler {
|
|
20
|
+
(request: Request): Promise<Response>;
|
|
21
|
+
}
|
|
22
|
+
export interface BotRuntime {
|
|
23
|
+
readonly token: string;
|
|
24
|
+
readonly allowedChatIds: readonly string[];
|
|
25
|
+
readonly adminChatIds: readonly string[];
|
|
26
|
+
readonly operatorChatIds: readonly string[];
|
|
27
|
+
readonly handleUpdate: UpdateHandler;
|
|
28
|
+
readonly createWebhookHandler: (options?: {
|
|
29
|
+
readonly secret?: string;
|
|
30
|
+
}) => WebhookHandler;
|
|
31
|
+
}
|
|
32
|
+
export declare function createNodeBot(config: BotRuntimeConfig, runtime?: RuntimeAdapter): BotRuntime;
|
|
33
|
+
export declare function createBunBot(config: BotRuntimeConfig, runtime?: RuntimeAdapter): BotRuntime;
|
|
34
|
+
export declare function createServerlessHandler(handler: UpdateHandler): (request: Request) => Promise<Response>;
|
|
35
|
+
export declare function createWorkerHandler(handler: UpdateHandler): (request: Request) => Promise<Response>;
|
|
36
|
+
export declare function createCronRunner<T = unknown>(runner: () => Promise<T> | T): () => Promise<{
|
|
37
|
+
ok: boolean;
|
|
38
|
+
data?: T;
|
|
39
|
+
id?: string;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function createWorkerRuntime<T = unknown>(runner: () => Promise<T> | T): () => Promise<{
|
|
42
|
+
ok: boolean;
|
|
43
|
+
data?: T;
|
|
44
|
+
id?: string;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function createWebhookHandler(handler: UpdateHandler, options?: {
|
|
47
|
+
readonly secret?: string;
|
|
48
|
+
}): WebhookHandler;
|
|
49
|
+
export interface PollingTransport {
|
|
50
|
+
getUpdates(offset?: number, timeoutSeconds?: number): Promise<readonly TelegramUpdate[]>;
|
|
51
|
+
}
|
|
52
|
+
export declare function runPolling(transport: PollingTransport, handler: UpdateHandler, runtime?: RuntimeAdapter, options?: {
|
|
53
|
+
readonly signal?: AbortSignal;
|
|
54
|
+
readonly timeoutSeconds?: number;
|
|
55
|
+
readonly retryMs?: number;
|
|
56
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function createRuntimeAdapter(fetcher = fetch) {
|
|
2
|
+
return {
|
|
3
|
+
fetch: fetcher,
|
|
4
|
+
now: () => new Date(),
|
|
5
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function buildBotRuntime(config, runtime = createRuntimeAdapter()) {
|
|
9
|
+
const handleUpdate = async (update) => {
|
|
10
|
+
const custom = config.onUpdate ?? (async (nextUpdate) => ({ ok: true, update: nextUpdate }));
|
|
11
|
+
return custom(update, runtime);
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
token: config.token,
|
|
15
|
+
allowedChatIds: config.allowedChatIds ?? [],
|
|
16
|
+
adminChatIds: config.adminChatIds ?? [],
|
|
17
|
+
operatorChatIds: config.operatorChatIds ?? [],
|
|
18
|
+
handleUpdate,
|
|
19
|
+
createWebhookHandler: (options = {}) => createWebhookHandler(handleUpdate, { secret: options.secret ?? config.webhookSecret }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createNodeBot(config, runtime = createRuntimeAdapter()) {
|
|
23
|
+
return buildBotRuntime(config, runtime);
|
|
24
|
+
}
|
|
25
|
+
export function createBunBot(config, runtime = createRuntimeAdapter()) {
|
|
26
|
+
return buildBotRuntime(config, runtime);
|
|
27
|
+
}
|
|
28
|
+
export function createServerlessHandler(handler) {
|
|
29
|
+
return createWebhookHandler(handler);
|
|
30
|
+
}
|
|
31
|
+
export function createWorkerHandler(handler) {
|
|
32
|
+
return createWebhookHandler(handler);
|
|
33
|
+
}
|
|
34
|
+
export function createCronRunner(runner) {
|
|
35
|
+
return async () => {
|
|
36
|
+
try {
|
|
37
|
+
const data = await runner();
|
|
38
|
+
return { ok: true, data, id: 'cron-runner' };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return { ok: false, id: 'cron-runner', data: undefined };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createWorkerRuntime(runner) {
|
|
46
|
+
return async () => {
|
|
47
|
+
try {
|
|
48
|
+
const data = await runner();
|
|
49
|
+
return { ok: true, data, id: 'worker-runtime' };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { ok: false, id: 'worker-runtime', data: undefined };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function createWebhookHandler(handler, options = {}) {
|
|
57
|
+
return async (request) => {
|
|
58
|
+
if (request.method !== 'POST')
|
|
59
|
+
return new Response('Method Not Allowed', { status: 405 });
|
|
60
|
+
if (options.secret && request.headers.get('x-telegram-bot-api-secret-token') !== options.secret) {
|
|
61
|
+
return new Response('Unauthorized', { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const update = await request.json();
|
|
65
|
+
await handler(update);
|
|
66
|
+
return Response.json({ ok: true });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return Response.json({ ok: false, error: error instanceof Error ? error.message : String(error) }, { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function runPolling(transport, handler, runtime = createRuntimeAdapter(), options = {}) {
|
|
74
|
+
let offset;
|
|
75
|
+
while (!options.signal?.aborted) {
|
|
76
|
+
try {
|
|
77
|
+
const updates = await transport.getUpdates(offset, options.timeoutSeconds ?? 25);
|
|
78
|
+
for (const update of updates) {
|
|
79
|
+
offset = update.update_id + 1;
|
|
80
|
+
await handler(update);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
await runtime.sleep(options.retryMs ?? 2_000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PackageConfig } from './config';
|
|
2
|
+
import { MikroTikService } from './mikrotik-service';
|
|
3
|
+
import { TelegramCommandRegistry } from './telegram-commands';
|
|
4
|
+
import type { CommandResponse, TelegramUpdate } from './types';
|
|
5
|
+
export interface NetPulseRuntime {
|
|
6
|
+
readonly registry: TelegramCommandRegistry;
|
|
7
|
+
readonly mikrotik: MikroTikService;
|
|
8
|
+
handleUpdate(update: TelegramUpdate): Promise<CommandResponse | undefined>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createRuntime(config: PackageConfig, fetcher?: typeof fetch): NetPulseRuntime;
|