diff --git a/src/adapter/hyperglass.ts b/src/adapter/hyperglass.ts index 62299d7..5289bca 100644 --- a/src/adapter/hyperglass.ts +++ b/src/adapter/hyperglass.ts @@ -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 { diff --git a/src/app.ts b/src/app.ts index 3b34882..8d379aa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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> = 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 & queryContext> = new Telegraf( + config.token +); bot.start(handleInfo(config.adapter)); -bot.launch(); \ No newline at end of file +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([ + 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}`); +}); diff --git a/src/handler/info.ts b/src/handler/info.ts index 9acb64c..98dd3c5 100644 --- a/src/handler/info.ts +++ b/src/handler/info.ts @@ -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 + }) } } \ No newline at end of file diff --git a/src/handler/list.ts b/src/handler/list.ts new file mode 100644 index 0000000..a561f7e --- /dev/null +++ b/src/handler/list.ts @@ -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 => `${key}: \n${map[key].map(dev=>`${dev.name}`).join("\n")}`).join("\n\n"),{ + reply_to_message_id: ctx.message?.message_id + }); + }; +} diff --git a/src/handler/query.ts b/src/handler/query.ts new file mode 100644 index 0000000..1f18bf6 --- /dev/null +++ b/src/handler/query.ts @@ -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; +} + +export default (capability: capabilities, adapter: adapter) => { + const queryScene = new Scenes.BaseScene(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 ${ctx.session.choosenDevice.name}.\n${result.output}`, + { 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 ${ctx.session.choosenDevice.name}.\n${result.output}`, + { parse_mode: "HTML" } + ); + }); + return queryScene; +}; diff --git a/src/lib/address.ts b/src/lib/address.ts new file mode 100644 index 0000000..1d8d381 --- /dev/null +++ b/src/lib/address.ts @@ -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"); +}; diff --git a/src/polyfill.ts b/src/polyfill.ts new file mode 100644 index 0000000..d22f9a4 --- /dev/null +++ b/src/polyfill.ts @@ -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); + }; +} diff --git a/src/types/adapter.d.ts b/src/types/adapter.d.ts index d967b10..46c9e8d 100644 --- a/src/types/adapter.d.ts +++ b/src/types/adapter.d.ts @@ -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; + target: import("ip-address").Address4 | import("ip-address").Address6, + ): Promise; }