First usable version

This commit is contained in:
186526 2023-01-18 01:51:54 +08:00
parent 2a5e1b74c9
commit 799e1ace7b
8 changed files with 235 additions and 14 deletions

View File

@ -1,5 +1,4 @@
import axios, { AxiosInstance } from "axios";
import { adapter, capabilities, device, info, queryError, querySuccess } from "../types/adapter";
import { Address4, Address6 } from 'ip-address';
interface deviceRaw {

View File

@ -1,10 +1,63 @@
import { Context, Telegraf } from 'telegraf';
import { Update } from 'typegram';
import handleInfo from './handler/info';
import config from "./config";
import { Context, Telegraf, session, Scenes } from "telegraf";
import { Update } from "typegram";
const bot: Telegraf<Context<Update>> = new Telegraf(config.token);
import handleList from "./handler/list";
import queryInit, { queryContext } from "./handler/query";
import handleInfo from "./handler/info";
import config from "./config";
import "./polyfill";
const bot: Telegraf<Context<Update> & queryContext> = new Telegraf(
config.token
);
bot.start(handleInfo(config.adapter));
bot.launch();
bot.help((ctx) => {
ctx.reply(
`/info - show information.
/list - show devices.
/route - show BGP route.
/ping - ping command.
/traceroute - traceroute command.
`,
{ reply_to_message_id: ctx.message?.message_id }
);
});
bot.use(session());
const stage = new Scenes.Stage<queryContext>([
queryInit("traceroute" as capabilities, config.adapter),
queryInit("ping" as capabilities, config.adapter),
queryInit("bgp_route" as capabilities, config.adapter),
]);
bot.use(stage.middleware());
bot.use((ctx, next) => {
console.log(
`INFO: Message from ${ctx.message?.from.id}@${ctx.message?.chat.id} in ${ctx.message?.date}`
);
if ((ctx.message?.date ?? 0) < ((new Date().getTime() / 1000) | 0) - 120) {
return;
}
next();
});
bot.command("info", handleInfo(config.adapter));
bot.command("list", handleList(config.adapter));
bot.command("route", (ctx) => ctx.scene.enter("bgp_route"));
bot.command("ping", (ctx) => ctx.scene.enter("ping"));
bot.command("traceroute", (ctx) => ctx.scene.enter("traceroute"));
process.once("SIGINT", () => bot.stop("SIGINT"));
process.once("SIGTERM", () => bot.stop("SIGTERM"));
bot.launch();
bot.catch((err, ctx) => {
console.log(ctx, err);
ctx.editMessageText(`${err}`);
});

View File

@ -1,9 +1,10 @@
import { Context } from "telegraf";
import { adapter } from "../types/adapter";
export default function handleInfo(adapter: adapter) {
return async (ctx: Context)=>{
const info = await adapter.info();
ctx.reply(`${info.name} Bot from ${info.organization}.`)
await ctx.reply(`${info.name} Bot from ${info.organization}.`, {
reply_to_message_id: ctx.message?.message_id
})
}
}

18
src/handler/list.ts Normal file
View File

@ -0,0 +1,18 @@
import { Context } from "telegraf";
export default function handleList(adapter: adapter) {
return async (ctx: Context) => {
const devices = await adapter.devices();
let map: { [key: string]: device[] } = {};
devices.forEach(
(device) =>
(map[device.group] =
typeof map[device.group] == "object"
? [...map[device.group], device]
: [device])
);
await ctx.replyWithHTML(Object.keys(map).map(key => `<b>${key}</b>: \n${map[key].map(dev=>`${dev.name}`).join("\n")}`).join("\n\n"),{
reply_to_message_id: ctx.message?.message_id
});
};
}

126
src/handler/query.ts Normal file
View File

@ -0,0 +1,126 @@
import { Context, deunionize, Scenes, session } from "telegraf";
import Address from "../lib/address";
import type { Address4, Address6 } from "ip-address";
interface querySession extends Scenes.SceneSession {
choosenDevice: device;
devices: device[];
address: Address4 | Address6;
}
export interface queryContext extends Context {
session: querySession;
scene: Scenes.SceneContextScene<queryContext>;
}
export default (capability: capabilities, adapter: adapter) => {
const queryScene = new Scenes.BaseScene<queryContext>(capability);
queryScene.enter(async (ctx) => {
let text = deunionize(ctx.message)?.text;
if (!text) return;
let command = text.split(" ").filter((k) => k);
if (command.length == 1) {
ctx.reply(`Usage: ${text} target [Device]`);
return;
}
const originMessage = await ctx.reply("Querying Looking Glass...", {
reply_to_message_id: ctx.message?.message_id,
});
try {
ctx.session.address = Address(command[1]);
} catch (e: any) {
await ctx.telegram.editMessageText(
originMessage.chat.id,
originMessage.message_id,
undefined,
e.toString()
);
return;
}
ctx.session.devices = await adapter.devices();
if (command.length == 2) {
await ctx.telegram.editMessageText(
originMessage.chat.id,
originMessage.message_id,
undefined,
"Please select the device you want to query about.",
{
reply_markup: {
inline_keyboard: ctx.session.devices.reduce(
(
pre: {
text: string;
callback_data: string;
}[][],
cur
) => {
if (pre[pre.length - 1].length == 2) pre.push([]);
pre[pre.length - 1].push({
text: cur.name,
callback_data: cur.name,
});
return pre;
},
[
[
{
text: "Random Device",
callback_data: "random",
},
],
]
),
},
}
);
return;
} else if (command.length == 3) {
ctx.session.choosenDevice =
ctx.session.devices.find((v) =>
v.name.toLocaleLowerCase().includes(command[2].toLocaleLowerCase())
) ?? ctx.session.devices[0];
}
const result = await adapter.query(
capability,
ctx.session.choosenDevice,
ctx.session.choosenDevice.vrfs[0],
ctx.session.address
);
await ctx.telegram.editMessageText(
originMessage.chat.id,
originMessage.message_id,
undefined,
`Query Result from <code>${ctx.session.choosenDevice.name}</code>.\n<code>${result.output}</code>`,
{ parse_mode: "HTML" }
);
});
queryScene.action(/.+/, async (ctx) => {
if (ctx.match[0] != "random") {
ctx.session.choosenDevice =
ctx.session.devices.find((v) => v.name.includes(ctx.match[0])) ??
ctx.session.devices[0];
} else
ctx.session.choosenDevice =
ctx.session.devices[(Math.random() * ctx.session.devices.length) | 0];
const result = await adapter.query(
capability,
ctx.session.choosenDevice,
ctx.session.choosenDevice.vrfs[0],
ctx.session.address
);
await ctx.editMessageText(
`Query Result from <code>${ctx.session.choosenDevice.name}</code>.\n<code>${result.output}</code>`,
{ parse_mode: "HTML" }
);
});
return queryScene;
};

8
src/lib/address.ts Normal file
View File

@ -0,0 +1,8 @@
import { Address4, Address6 } from "ip-address";
import { isIPv4, isIPv6 } from "net";
export default (address: string): Address4 | Address6 => {
if (isIPv4(address)) return new Address4(address);
if (isIPv6(address)) return new Address6(address);
throw new Error("Invalid address");
};

18
src/polyfill.ts Normal file
View File

@ -0,0 +1,18 @@
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (
searchValue: string | RegExp,
replaceValue: string | ((substring: string, ...args: any[]) => string)
): string {
if (typeof replaceValue == "function") throw new Error("Not Supported.");
// If a regex pattern
if (
Object.prototype.toString.call(searchValue).toLowerCase() ===
"[object regexp]"
) {
return this.replace(searchValue, replaceValue);
}
// If a string
return this.replace(new RegExp(searchValue, "g"), replaceValue);
};
}

View File

@ -1,5 +1,3 @@
import { Address4, Address6 } from "ip-address";
enum capabilities {
"BGP Route" = "bgp_route",
"Ping" = "ping",
@ -19,7 +17,7 @@ interface device {
}
interface queryResponse {
level: "success" | "warning" | "error" | "danger";
level: "warning" | "error" | "danger";
}
interface queryError extends queryResponse {
@ -39,6 +37,6 @@ interface adapter {
capability: capabilities,
device: device,
vrf: string,
target: Address4 | Address6
): Promise<queryError | querySuccess | queryResponse>;
target: import("ip-address").Address4 | import("ip-address").Address6,
): Promise<queryError | querySuccess>;
}