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();