diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..0a8642f --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Zeppelin ignored files +/ZeppelinRemoteNotebooks/ diff --git a/.idea/bot.iml b/.idea/bot.iml new file mode 100644 index 0000000..18ec59d --- /dev/null +++ b/.idea/bot.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..85e3a4b --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,63 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..307554b --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..2f80ea3 --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..25cd7e1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..869252d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/app.js b/src/app.js index 9e6e99b..4f49f79 100644 --- a/src/app.js +++ b/src/app.js @@ -1,11 +1,12 @@ -import { memoryUsage } from "process"; -import { fork } from "child_process"; +import { memoryUsage } from "node:process"; +import { fork } from "node:child_process"; import { Telegraf } from "telegraf"; import dotenv from "dotenv"; import mongoose from "mongoose"; import Datastore from "@teknologi-umum/nedb-promises"; +import * as Sentry from "@sentry/node"; -import { sentry, terminal, logger } from "#utils/logger/index.js"; +import { terminal, logger } from "#utils/logger/index.js"; import { pathTo } from "#utils/path.js"; import * as poll from "#services/poll/index.js"; @@ -25,9 +26,24 @@ import * as analytics from "#services/analytics/index.js"; import * as news from "#services/news/index.js"; import * as qr from "#services/qr/index.js"; import * as pesto from "#services/pesto/index.js"; +import { getCommandName } from "#utils/command.js"; dotenv.config({ path: pathTo(import.meta.url, "../.env") }); + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + enabled: process.env.NODE_ENV === "production", + environment: process.env.NODE_ENV, + sampleRate: 1.0, + tracesSampleRate: 0.2, + integrations: [ + new Sentry.Integrations.Http({ tracing: true }), + new Sentry.Integrations.Undici(), + ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations() + ] +}); + const bot = new Telegraf(process.env.BOT_TOKEN); const cache = Datastore.create(); const mongo = mongoose.createConnection(String(process.env.MONGO_URL), { @@ -37,11 +53,12 @@ const mongo = mongoose.createConnection(String(process.env.MONGO_URL), { // Fork processes const hackernewsFork = fork(pathTo(import.meta.url, "./hackernews.js"), { detached: true }); -function terminate(caller) { +async function terminate(caller) { const t = Date.now(); - mongo.close(); bot.stop(caller); hackernewsFork.kill(); + await mongo.close(); + await Sentry.flush(); terminal.info(`${caller}: ${Date.now() - t}ms`); } @@ -58,6 +75,29 @@ async function main() { return; } + // TODO: Move this somewhere else + const validCommands = ["blidingej", "covid", "devread", "dukun", "eval", "laodeai", "news", "hilih", "joke", "kktbsys", "yntks", "homework", "illuminati", "c", "cpp", "clisp", "dotnet", "go", "java", "js", "julia", "lua", "php", "python", "ruby", "sqlite3", "ts", "v", "brainfuck", "qr", "quote", "search", "snap"]; + if (ctx.updateType === "message") { + const command = getCommandName(ctx); + if (command === "" || !validCommands.includes(command)) { + next(); + return; + } + + Sentry.startSpan({ + name: command, + op: "bot.update", + data: { + from_username: ctx.from.username, + chat_type: ctx.message.chat.type, + chat_title: ctx.message.chat.title + } + }, () => { + next(); + }); + return; + } + next(); }); @@ -83,11 +123,11 @@ async function main() { .filter((v) => Array.isArray(v)) .flat(); - bot.telegram.setMyCommands(commands); + await bot.telegram.setMyCommands(commands); bot.catch(async (error, context) => { try { - sentry.captureException(error, (scope) => { + Sentry.captureException(error, (scope) => { scope.setContext("chat", { chat_id: context.message.chat.id, chat_title: context.message.chat.title, diff --git a/src/hackernews.js b/src/hackernews.js index 4e29ca1..4fa91a7 100644 --- a/src/hackernews.js +++ b/src/hackernews.js @@ -1,8 +1,8 @@ import { Telegraf } from "telegraf"; import dotenv from "dotenv"; +import * as Sentry from "@sentry/node"; import { pathTo } from "#utils/path.js"; import { run } from "#services/hackernews/index.js"; -import { sentry } from "#utils/logger/index.js"; dotenv.config({ path: pathTo(import.meta.url, "../.env") }); @@ -19,7 +19,7 @@ for (;;) { // eslint-disable-next-line no-await-in-loop await run(bot) .catch((error) => { - sentry.captureException(error); + Sentry.captureException(error); }) .finally(() => { done = true; diff --git a/src/services/meme/index.js b/src/services/meme/index.js index 9775e57..0a094de 100644 --- a/src/services/meme/index.js +++ b/src/services/meme/index.js @@ -1,9 +1,9 @@ import got, { TimeoutError } from "got"; +import * as Sentry from "@sentry/node"; import { randomNumber } from "carret"; import { DEFAULT_HEADERS } from "#utils/http.js"; import { isBigGroup, isHomeGroup } from "#utils/home.js"; import { logger } from "#utils/logger/logtail.js"; -import { sentry } from "#utils/logger/index.js"; /** * Send memes.. @@ -125,7 +125,7 @@ export function register(bot, cache) { }); } catch (error) { if (error instanceof TimeoutError) { - sentry.addBreadcrumb({ + Sentry.addBreadcrumb({ type: "default", level: "warning", category: "http.request", @@ -133,7 +133,7 @@ export function register(bot, cache) { data: error }); - sentry.captureException(error, { + Sentry.captureException(error, { level: "warning", extra: { chat: { diff --git a/src/services/qr/index.js b/src/services/qr/index.js index 285e61f..aac9b9f 100644 --- a/src/services/qr/index.js +++ b/src/services/qr/index.js @@ -1,7 +1,7 @@ import QRCode from "qrcode"; import { getCommandArgs } from "#utils/command.js"; import { logger } from "#utils/logger/logtail.js"; -import { sentry } from "#utils/logger/index.js"; +import * as Sentry from "@sentry/node"; /** * Handling /qr command @@ -36,7 +36,7 @@ async function qr(context) { "Oppss.. Service sedang error.." ); - sentry.captureException(err, { + Sentry.getCurrentHub().captureException(err, { level: "warning", extra: { chat: { diff --git a/src/services/snap/index.js b/src/services/snap/index.js index 384d442..bc56fdc 100644 --- a/src/services/snap/index.js +++ b/src/services/snap/index.js @@ -1,4 +1,5 @@ -import { logger, sentry } from "#utils/logger/index.js"; +import * as Sentry from "@sentry/node"; +import { logger } from "#utils/logger/index.js"; import { getCommandArgs } from "#utils/command.js"; import { terminal } from "#utils/logger/terminal.js"; import { generateImage } from "./utils.js"; @@ -101,7 +102,7 @@ async function snap(context) { } if (err instanceof TimeoutError) { - sentry.addBreadcrumb({ + Sentry.getCurrentHub().addBreadcrumb({ type: "default", level: "warning", category: "http.request", @@ -109,7 +110,7 @@ async function snap(context) { data: err }); - sentry.captureException(err, { + Sentry.getCurrentHub().captureException(err, { level: "warning", extra: { chat: { diff --git a/src/utils/command.js b/src/utils/command.js index 12cd2e2..3e4e135 100644 --- a/src/utils/command.js +++ b/src/utils/command.js @@ -49,3 +49,29 @@ export const getCommandArgs = (cmd, context) => { return context.message.text; }; + +/** + * Get command name from given context + * @param {import("telegraf").Context} context - The Telegraf context + * @return {string} command argument, empty string if context is not a command + */ +export const getCommandName = (context) => { + if (context === undefined || context === null) { + return ""; + } + + const text = context.message.text; + if (text && !text.startsWith("/")) { + return ""; + } + + const command = text + .substring(1) + .split(/\s/) + .at(0); + if (command === undefined) { + return ""; + } + + return command.replace(`@${context.me}`, ""); +}; diff --git a/src/utils/logger/index.js b/src/utils/logger/index.js index f87a3c8..80c0ae0 100644 --- a/src/utils/logger/index.js +++ b/src/utils/logger/index.js @@ -1,3 +1,2 @@ export * from "./logtail.js"; -export * from "./sentry.js"; export * from "./terminal.js"; \ No newline at end of file diff --git a/src/utils/logger/sentry.js b/src/utils/logger/sentry.js deleted file mode 100644 index 5d6bf8f..0000000 --- a/src/utils/logger/sentry.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as sentry from "@sentry/node"; - -sentry.init({ - dsn: process.env.SENTRY_DSN, - enabled: process.env.NODE_ENV === "production", - environment: process.env.NODE_ENV, - tracesSampleRate: 1.0 -}); - -export { sentry }; diff --git a/tests/command.test.js b/tests/command.test.js index a80b5e8..be08c34 100644 --- a/tests/command.test.js +++ b/tests/command.test.js @@ -1,6 +1,6 @@ import { test } from "uvu"; import * as assert from "uvu/assert"; -import { getCommandArgs } from "../src/utils/command.js"; +import { getCommandArgs, getCommandName } from "../src/utils/command.js"; test("should get argument if command is invoked without username", () => { const fakeContext = { @@ -68,4 +68,49 @@ test("should handle complicated new lines", () => { assert.equal(argument, "async function hello() {\n\tconst logger = new Logger();\r\n\tlogger.log(\"Hello world\");\r};"); }); +test("should be able to parse complicated command name", () => { + const fakeContext = { + message: { text: "/laodeai\r\nasync function hello() {\n\tconst logger = new Logger();\r\n\tlogger.log(\"Hello world\");\r};\n" }, + me: "teknologiumumbot" + }; + + const command = getCommandName(fakeContext); + + assert.equal(command, "laodeai"); +}); + +test("should be able to parse command name with name", () => { + const fakeContext = { + message: { text: "/dukun@teknologiumumbot" }, + me: "teknologiumumbot" + }; + + const command = getCommandName(fakeContext); + + assert.equal(command, "dukun"); +}); + +test("should return empty string for undefined and null context", () => { + assert.equal(getCommandName(undefined), ""); + assert.equal(getCommandName(null), ""); +}); + +test("should return empty string for non command", () => { + const fakeContext = { + message: { text: "foo bar" }, + me: "teknologiumumbot" + }; + + assert.equal(getCommandName(fakeContext), ""); +}); + +test("should return empty string for just slash", () => { + const fakeContext = { + message: { text: "/" }, + me: "teknologiumumbot" + }; + + assert.equal(getCommandName(fakeContext), ""); +}); + test.run();