From 9b4a334cdd18be0b82faf4b7dd31f45ef779d07c Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 11 Mar 2026 14:40:37 +0200 Subject: [PATCH 01/57] Created empty module for bot logic --- .../custom/wisetrout_do_bot/wisetrout_do_bot.info.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.info.yml diff --git a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.info.yml b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.info.yml new file mode 100644 index 0000000..e9c6707 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.info.yml @@ -0,0 +1,6 @@ +name: Wisetrout DO bot Module +description: Enables Wisetrout DO bot in Telegram +package: Custom + +type: module +core_version_requirement: ^10.3 || ^11 \ No newline at end of file From 3b0e69a703bfcfe27b803a6d8c802bfde51dad2a Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 11 Mar 2026 14:43:39 +0200 Subject: [PATCH 02/57] feature: bot subscription (dummy api calls for now) --- wisetrout-do-bot/.dockerignore | 4 + wisetrout-do-bot/Dockerfile | 6 + wisetrout-do-bot/api-calls/modules-list.js | 24 + wisetrout-do-bot/api-calls/subscription.js | 11 + wisetrout-do-bot/bot.js | 34 ++ wisetrout-do-bot/commands/start.js | 18 + wisetrout-do-bot/compose.yaml | 11 + .../middlewares/preferences-middleware.js | 10 + .../middlewares/subscription-middleware.js | 111 ++++ wisetrout-do-bot/package-lock.json | 540 ++++++++++++++++++ wisetrout-do-bot/package.json | 17 + 11 files changed, 786 insertions(+) create mode 100644 wisetrout-do-bot/.dockerignore create mode 100644 wisetrout-do-bot/Dockerfile create mode 100644 wisetrout-do-bot/api-calls/modules-list.js create mode 100644 wisetrout-do-bot/api-calls/subscription.js create mode 100644 wisetrout-do-bot/bot.js create mode 100644 wisetrout-do-bot/commands/start.js create mode 100644 wisetrout-do-bot/compose.yaml create mode 100644 wisetrout-do-bot/middlewares/preferences-middleware.js create mode 100644 wisetrout-do-bot/middlewares/subscription-middleware.js create mode 100644 wisetrout-do-bot/package-lock.json create mode 100644 wisetrout-do-bot/package.json diff --git a/wisetrout-do-bot/.dockerignore b/wisetrout-do-bot/.dockerignore new file mode 100644 index 0000000..5b0e72a --- /dev/null +++ b/wisetrout-do-bot/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +.gitignore +.env \ No newline at end of file diff --git a/wisetrout-do-bot/Dockerfile b/wisetrout-do-bot/Dockerfile new file mode 100644 index 0000000..8e5392a --- /dev/null +++ b/wisetrout-do-bot/Dockerfile @@ -0,0 +1,6 @@ +FROM node:lts-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +CMD ["npm", "start"] \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/modules-list.js b/wisetrout-do-bot/api-calls/modules-list.js new file mode 100644 index 0000000..5363cee --- /dev/null +++ b/wisetrout-do-bot/api-calls/modules-list.js @@ -0,0 +1,24 @@ +export async function getModulesList(){ + return [ + { + name: "Ctools", + machine_name: "ctools" + }, + { + name: "Meta tag", + machine_name: "metatag" + }, + { + name: "Commerce", + machine_name: "commerce" + }, + { + name: "AI", + machine_name: "ai" + }, + { + name: "Facebook", + machine_name: "facebook" + } + ] +} \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js new file mode 100644 index 0000000..db510dc --- /dev/null +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -0,0 +1,11 @@ +export async function subscribe(chatId, modules){} + +export async function unsubscribe(chatId){} + +export async function checkSubscription(chatId){ + return { + subscribed: false + }; +} + +export async function wipeoutData(chatId){} \ No newline at end of file diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js new file mode 100644 index 0000000..6c0c55e --- /dev/null +++ b/wisetrout-do-bot/bot.js @@ -0,0 +1,34 @@ +import { Markup, Telegraf, session } from 'telegraf'; +import preferencesMiddleware from './middlewares/preferences-middleware.js'; +import { subscribe, unsubscribe } from './api-calls/subscription.js'; +import startHandler from './commands/start.js'; +import { subscriptionMiddleware } from './middlewares/subscription-middleware.js'; + +const bot = new Telegraf(process.env.BOT_TOKEN); + +bot.use(session()); + +bot.use(preferencesMiddleware); +bot.use(subscriptionMiddleware) + +bot.start(startHandler); + +bot.action('sub', ctx => { + ctx.scene.enter('subscription-menu'); +}) + +bot.action('unsub', async ctx => { + await Promise.all([ + ctx.reply('Cancelling subscription...'), + unsubscribe(ctx.from.id) + ]); + + ctx.session.subscribed = false; + ctx.reply('Subscription cancelled'); +}); + +bot.launch(); + +// Enable graceful stop +process.once('SIGINT', () => bot.stop('SIGINT')) +process.once('SIGTERM', () => bot.stop('SIGTERM')) \ No newline at end of file diff --git a/wisetrout-do-bot/commands/start.js b/wisetrout-do-bot/commands/start.js new file mode 100644 index 0000000..6667fe8 --- /dev/null +++ b/wisetrout-do-bot/commands/start.js @@ -0,0 +1,18 @@ +import { Markup} from 'telegraf'; + + +export default function startHandler(ctx){ + + const subBtn = Markup.button.callback('πŸ“¬ ' + 'Subscribe to updates', 'sub'); + const unsubBtn = Markup.button.callback('πŸ“­ ' + 'Unsubscribe from all updates', 'unsub'); + const updateSubBtn = Markup.button.callback('βš™οΈ ' + 'Change subscription', 'sub'); + + const keyboardRows = ctx.session.subscribed ? + [[updateSubBtn], [unsubBtn]]: + [[subBtn]] + + ctx.reply( + 'Welcome to the Wisetrout Drupal org bot!', + Markup.inlineKeyboard(keyboardRows) + ) +} \ No newline at end of file diff --git a/wisetrout-do-bot/compose.yaml b/wisetrout-do-bot/compose.yaml new file mode 100644 index 0000000..81bd251 --- /dev/null +++ b/wisetrout-do-bot/compose.yaml @@ -0,0 +1,11 @@ +services: + do-bot-app: + container_name: do-bot-app + build: + context: . + dockerfile: Dockerfile + env_file: '.env' + volumes: + - type: bind + source: . + target: /app \ No newline at end of file diff --git a/wisetrout-do-bot/middlewares/preferences-middleware.js b/wisetrout-do-bot/middlewares/preferences-middleware.js new file mode 100644 index 0000000..ccdfa1e --- /dev/null +++ b/wisetrout-do-bot/middlewares/preferences-middleware.js @@ -0,0 +1,10 @@ +import { checkSubscription } from "../api-calls/subscription.js"; + + +export default async function preferencesMiddleware(ctx, next){ + if(!ctx.session){ + const {subscribed, modules} = await checkSubscription(ctx.from.id); + ctx.session = { subscribed, modules }; + } + next(); +} \ No newline at end of file diff --git a/wisetrout-do-bot/middlewares/subscription-middleware.js b/wisetrout-do-bot/middlewares/subscription-middleware.js new file mode 100644 index 0000000..a25ffc3 --- /dev/null +++ b/wisetrout-do-bot/middlewares/subscription-middleware.js @@ -0,0 +1,111 @@ +import { Markup, Scenes } from "telegraf"; +import { getModulesList } from "../api-calls/modules-list.js"; +import { subscribe } from "../api-calls/subscription.js"; + +const scene = new Scenes.BaseScene('subscription-menu'); + +scene.enter(async ctx => { + ctx.reply("Subscribe to:", + Markup.inlineKeyboard([ + [ + Markup.button.callback("βœ… All updates", "all"), + Markup.button.callback("βš™οΈ Specific modules", "specific") + ], + [ + Markup.button.callback("🚫 Cancel", "cancel") + ] + ])) +}); + +scene.action("all", async ctx => { + await Promise.all([ + ctx.reply('Subscribing to updates...'), + subscribe(ctx.from.id) + ]); + + ctx.session.subscribed = true; + ctx.reply('Subscription successful!'); + ctx.scene.leave(); +}) + +scene.action("specific", async ctx => { + + const [modules] = await Promise.all([ + getModulesList(), + ctx.reply("Loading...") + ]) + ; + + ctx.session.modules = modules; + ctx.session.selectedModules = []; + + await ctx.reply('Please pick the modules you are interested in and click "subscribe".', + Markup.inlineKeyboard(createKeyboardRows(modules)) + ); +}) + +scene.action("complete", async ctx => { + await Promise.all([ + ctx.reply('Subscribing to updates...'), + subscribe(ctx.from.id, ctx.session.selectedModules) + ]); + ctx.session.subscribed = true; + delete ctx.session.selectedModule; + delete ctx.session.modules; + ctx.reply('Subscription successful!'); + ctx.scene.leave(); +}) + + + +scene.action("cancel", ctx => { + ctx.reply("Subscription process cancelled"); + if(ctx.session.selectedModules) delete ctx.session.selectedModules; + if(ctx.session.modules) delete ctx.session.modules; + ctx.scene.leave(); +}); + +scene.on("callback_query", async ctx => { + const [prefix, action, moduleMachineName] = ctx.callbackQuery.data.split('--'); + if(prefix != "module") return; + + if(action === "select"){ + ctx.session.selectedModules.push(moduleMachineName); + }else{ + ctx.session.selectedModules = ctx.session.selectedModules.filter(mn => mn != moduleMachineName); + } + ctx.editMessageReplyMarkup( + { + inline_keyboard: createKeyboardRows(ctx.session.modules, ctx.session.selectedModules) + } + ); +}); + +const stage = new Scenes.Stage([scene]); +export const subscriptionMiddleware = stage.middleware(); + + +function createKeyboardRows(allModules, selectedModules = []){ + const keyboardRows = []; + + allModules.forEach((module, moduleIndex) => { + + const {name, machine_name} = module; + const moduleSelected = selectedModules.includes(machine_name); + const btnText = `${moduleSelected ? "βœ”οΈ ":""}${name}`; + const btnAction = `module--${moduleSelected ? "deselect":"select"}--${machine_name}`; + + const btn = Markup.button.callback(btnText, btnAction); + + if(moduleIndex % 3 === 0){ + keyboardRows.push([btn]); + }else{ + keyboardRows[keyboardRows.length - 1].push(btn); + } + }); + + keyboardRows.push([Markup.button.callback("πŸ“© subscribe", "complete")]); + keyboardRows.push([Markup.button.callback("🚫 Cancel", "cancel")]); + + return keyboardRows; +} \ No newline at end of file diff --git a/wisetrout-do-bot/package-lock.json b/wisetrout-do-bot/package-lock.json new file mode 100644 index 0000000..99db303 --- /dev/null +++ b/wisetrout-do-bot/package-lock.json @@ -0,0 +1,540 @@ +{ + "name": "wisetrout_do_bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wisetrout_do_bot", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "telegraf": "^4.16.3" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } + }, + "node_modules/@telegraf/types": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", + "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-timeout": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", + "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-compare": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", + "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.2.0" + } + }, + "node_modules/sandwich-stream": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", + "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/telegraf": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", + "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", + "license": "MIT", + "dependencies": { + "@telegraf/types": "^7.1.0", + "abort-controller": "^3.0.0", + "debug": "^4.3.4", + "mri": "^1.2.0", + "node-fetch": "^2.7.0", + "p-timeout": "^4.1.0", + "safe-compare": "^1.1.4", + "sandwich-stream": "^2.0.2" + }, + "bin": { + "telegraf": "lib/cli.mjs" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/wisetrout-do-bot/package.json b/wisetrout-do-bot/package.json new file mode 100644 index 0000000..b3a2c13 --- /dev/null +++ b/wisetrout-do-bot/package.json @@ -0,0 +1,17 @@ +{ + "name": "wisetrout_do_bot", + "version": "1.0.0", + "description": "", + "main": "bot.js", + "scripts": { + "start": "nodemon bot.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "telegraf": "^4.16.3" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } +} From b7f0d37c52d0bfcf73b79f5dc75c4b43e4ed90f6 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 12 Mar 2026 12:32:57 +0200 Subject: [PATCH 03/57] feat: logic for unsubscribing from bot --- wisetrout-do-bot/Dockerfile | 6 +-- wisetrout-do-bot/api-calls/subscription.js | 14 +++-- wisetrout-do-bot/bot.js | 53 +++++++++++++++---- wisetrout-do-bot/commands/start.js | 18 ------- wisetrout-do-bot/markup/buttons.js | 8 +++ wisetrout-do-bot/markup/keyboards.js | 18 +++++++ .../middlewares/preferences-middleware.js | 4 +- .../subscription-menu.js} | 27 ++++++---- wisetrout-do-bot/scenes/wipeout.js | 32 +++++++++++ 9 files changed, 133 insertions(+), 47 deletions(-) delete mode 100644 wisetrout-do-bot/commands/start.js create mode 100644 wisetrout-do-bot/markup/buttons.js create mode 100644 wisetrout-do-bot/markup/keyboards.js rename wisetrout-do-bot/{middlewares/subscription-middleware.js => scenes/subscription-menu.js} (82%) create mode 100644 wisetrout-do-bot/scenes/wipeout.js diff --git a/wisetrout-do-bot/Dockerfile b/wisetrout-do-bot/Dockerfile index 8e5392a..8e35885 100644 --- a/wisetrout-do-bot/Dockerfile +++ b/wisetrout-do-bot/Dockerfile @@ -1,6 +1,6 @@ FROM node:lts-alpine WORKDIR /app -COPY package*.json ./ -RUN npm install +# COPY package*.json ./ +# RUN npm install COPY . . -CMD ["npm", "start"] \ No newline at end of file +CMD ["sh", "-c", "npm install && npm start"] \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index db510dc..2f3fd82 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -3,9 +3,15 @@ export async function subscribe(chatId, modules){} export async function unsubscribe(chatId){} export async function checkSubscription(chatId){ - return { - subscribed: false - }; + await wait(2000); + return null; } -export async function wipeoutData(chatId){} \ No newline at end of file +export async function wipeoutData(chatId){} + +// Imitation of server request taking time +async function wait(ms){ + return await new Promise((res, rej) => { + setTimeout(() => res(), ms) + }) +} \ No newline at end of file diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 6c0c55e..06e1c6d 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -1,17 +1,26 @@ -import { Markup, Telegraf, session } from 'telegraf'; +import { Telegraf, session } from 'telegraf'; import preferencesMiddleware from './middlewares/preferences-middleware.js'; +import { scene as subscrScene } from './scenes/subscription-menu.js'; +import { createActionsKeyboard } from './markup/keyboards.js'; +import { scene as wipeoutScene } from './scenes/wipeout.js'; import { subscribe, unsubscribe } from './api-calls/subscription.js'; -import startHandler from './commands/start.js'; -import { subscriptionMiddleware } from './middlewares/subscription-middleware.js'; +import { Stage } from 'telegraf/scenes'; const bot = new Telegraf(process.env.BOT_TOKEN); bot.use(session()); +const stage = new Stage([subscrScene, wipeoutScene]); + bot.use(preferencesMiddleware); -bot.use(subscriptionMiddleware) +bot.use(stage.middleware()); -bot.start(startHandler); +bot.start(ctx => { + ctx.reply( + 'Welcome to the Wisetrout Drupal org bot!', + createActionsKeyboard(ctx) + ) +}); bot.action('sub', ctx => { ctx.scene.enter('subscription-menu'); @@ -19,14 +28,40 @@ bot.action('sub', ctx => { bot.action('unsub', async ctx => { await Promise.all([ - ctx.reply('Cancelling subscription...'), + ctx.reply('Cancelling your subscription...'), unsubscribe(ctx.from.id) ]); - - ctx.session.subscribed = false; - ctx.reply('Subscription cancelled'); + + ctx.session.userInfo.subscribed = false; + ctx.reply('Unsubscribed from all updates. We will keep your preferences saved in case you want to renew your subscription.', + createActionsKeyboard(ctx) + ); }); +bot.action('resub', async ctx => { + + await Promise.all([ + ctx.reply('Reactivating your subscription...'), + subscribe(ctx.from.id, ctx.session.userInfo.modules) + ]); + + ctx.session.userInfo.subscribed = true; + ctx.reply('Subscription reactivated', + createActionsKeyboard(ctx) + ); + +}) + +bot.action('wipeout', async ctx => { + ctx.scene.enter('wipeout'); +}); + +// bot.action('help', async ctx => { +// ctx.reply('You may use the following commands to manage your subscription:', +// createActionsKeyboard(ctx) +// ); +// }); + bot.launch(); // Enable graceful stop diff --git a/wisetrout-do-bot/commands/start.js b/wisetrout-do-bot/commands/start.js deleted file mode 100644 index 6667fe8..0000000 --- a/wisetrout-do-bot/commands/start.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Markup} from 'telegraf'; - - -export default function startHandler(ctx){ - - const subBtn = Markup.button.callback('πŸ“¬ ' + 'Subscribe to updates', 'sub'); - const unsubBtn = Markup.button.callback('πŸ“­ ' + 'Unsubscribe from all updates', 'unsub'); - const updateSubBtn = Markup.button.callback('βš™οΈ ' + 'Change subscription', 'sub'); - - const keyboardRows = ctx.session.subscribed ? - [[updateSubBtn], [unsubBtn]]: - [[subBtn]] - - ctx.reply( - 'Welcome to the Wisetrout Drupal org bot!', - Markup.inlineKeyboard(keyboardRows) - ) -} \ No newline at end of file diff --git a/wisetrout-do-bot/markup/buttons.js b/wisetrout-do-bot/markup/buttons.js new file mode 100644 index 0000000..d2aeff2 --- /dev/null +++ b/wisetrout-do-bot/markup/buttons.js @@ -0,0 +1,8 @@ +import { Markup } from "telegraf"; + +export const subBtn = Markup.button.callback('πŸ“¬ Subscribe to updates', 'sub'); +export const unsubBtn = Markup.button.callback('πŸ“­ Unsubscribe from all updates', 'unsub'); +export const updateSubBtn = Markup.button.callback('βš™οΈ Change subscription', 'sub'); +export const resubBtn = Markup.button.callback('πŸ“¬ Resubscribe to updates', 'resub'); +export const cancelBtn = Markup.button.callback("🚫 Cancel", "cancel"); +export const wipeoutBtn = Markup.button.callback('πŸ—‘οΈ Wipe out my data', 'wipeout'); diff --git a/wisetrout-do-bot/markup/keyboards.js b/wisetrout-do-bot/markup/keyboards.js new file mode 100644 index 0000000..c1d134f --- /dev/null +++ b/wisetrout-do-bot/markup/keyboards.js @@ -0,0 +1,18 @@ +import { Markup } from "telegraf"; +import { resubBtn, subBtn, unsubBtn, updateSubBtn, wipeoutBtn } from "./buttons.js"; + +export function createActionsKeyboard(ctx){ + let keyboardRows; + + if(ctx.session.userInfo){ + if(ctx.session.userInfo.subscribed){ + keyboardRows = [[updateSubBtn], [unsubBtn]]; + }else{ + keyboardRows = [[resubBtn, wipeoutBtn]]; + } + }else{ + keyboardRows = [[subBtn]]; + } + + return Markup.inlineKeyboard(keyboardRows); +} \ No newline at end of file diff --git a/wisetrout-do-bot/middlewares/preferences-middleware.js b/wisetrout-do-bot/middlewares/preferences-middleware.js index ccdfa1e..f033d28 100644 --- a/wisetrout-do-bot/middlewares/preferences-middleware.js +++ b/wisetrout-do-bot/middlewares/preferences-middleware.js @@ -3,8 +3,8 @@ import { checkSubscription } from "../api-calls/subscription.js"; export default async function preferencesMiddleware(ctx, next){ if(!ctx.session){ - const {subscribed, modules} = await checkSubscription(ctx.from.id); - ctx.session = { subscribed, modules }; + const userInfo = await checkSubscription(ctx.from.id); + ctx.session = { userInfo }; } next(); } \ No newline at end of file diff --git a/wisetrout-do-bot/middlewares/subscription-middleware.js b/wisetrout-do-bot/scenes/subscription-menu.js similarity index 82% rename from wisetrout-do-bot/middlewares/subscription-middleware.js rename to wisetrout-do-bot/scenes/subscription-menu.js index a25ffc3..16edecb 100644 --- a/wisetrout-do-bot/middlewares/subscription-middleware.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -2,7 +2,7 @@ import { Markup, Scenes } from "telegraf"; import { getModulesList } from "../api-calls/modules-list.js"; import { subscribe } from "../api-calls/subscription.js"; -const scene = new Scenes.BaseScene('subscription-menu'); +export const scene = new Scenes.BaseScene('subscription-menu'); scene.enter(async ctx => { ctx.reply("Subscribe to:", @@ -23,7 +23,10 @@ scene.action("all", async ctx => { subscribe(ctx.from.id) ]); - ctx.session.subscribed = true; + ctx.session.userInfo = { + subscribed: true, + modules: [] + } ctx.reply('Subscription successful!'); ctx.scene.leave(); }) @@ -37,10 +40,10 @@ scene.action("specific", async ctx => { ; ctx.session.modules = modules; - ctx.session.selectedModules = []; + ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; await ctx.reply('Please pick the modules you are interested in and click "subscribe".', - Markup.inlineKeyboard(createKeyboardRows(modules)) + Markup.inlineKeyboard(createKeyboardRows(ctx)) ); }) @@ -49,7 +52,10 @@ scene.action("complete", async ctx => { ctx.reply('Subscribing to updates...'), subscribe(ctx.from.id, ctx.session.selectedModules) ]); - ctx.session.subscribed = true; + ctx.session.userInfo = { + subscribed: true, + modules: ctx.session.selectedModules + } delete ctx.session.selectedModule; delete ctx.session.modules; ctx.reply('Subscription successful!'); @@ -76,19 +82,18 @@ scene.on("callback_query", async ctx => { } ctx.editMessageReplyMarkup( { - inline_keyboard: createKeyboardRows(ctx.session.modules, ctx.session.selectedModules) + inline_keyboard: createKeyboardRows(ctx) } ); }); -const stage = new Scenes.Stage([scene]); -export const subscriptionMiddleware = stage.middleware(); - -function createKeyboardRows(allModules, selectedModules = []){ +function createKeyboardRows(ctx){ const keyboardRows = []; - allModules.forEach((module, moduleIndex) => { + const {modules, selectedModules} = ctx.session; + + modules.forEach((module, moduleIndex) => { const {name, machine_name} = module; const moduleSelected = selectedModules.includes(machine_name); diff --git a/wisetrout-do-bot/scenes/wipeout.js b/wisetrout-do-bot/scenes/wipeout.js new file mode 100644 index 0000000..0e33130 --- /dev/null +++ b/wisetrout-do-bot/scenes/wipeout.js @@ -0,0 +1,32 @@ +import { Markup, Scenes } from "telegraf"; +import { wipeoutData } from "../api-calls/subscription.js"; + +export const scene = new Scenes.BaseScene('wipeout'); + +scene.enter(async ctx => { + ctx.reply("⚠️ Are you sure you would like to wipe out all of your data? This action will clear the list of modules you are interested in. If you decide to resubscribe, you will need to select them again.", + Markup.inlineKeyboard([ + [ + Markup.button.callback("βœ… Delete my data", "delete") + ], + [ + Markup.button.callback("🚫 Cancel", "cancel") + ] + ])) +}); + +scene.action("delete", async ctx => { + await Promise.all([ + ctx.reply('Clearing your data...'), + wipeoutData(ctx.from.id) + ]); + + ctx.session.userInfo = null; + ctx.reply('Cleared successfully!'); + ctx.scene.leave(); +}) + +scene.action("cancel", ctx => { + ctx.reply("Deletion cancelled"); + ctx.scene.leave(); +}); \ No newline at end of file From deec2d58265dd9a891fe689a69ba670b8344d836 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 16 Mar 2026 11:24:14 +0200 Subject: [PATCH 04/57] feat: created Telegram endpoints (empty for now) --- composer.json | 1 + composer.lock | 932 ++++++++++-------- .../src/Plugin/rest/resource/ModulesList.php | 37 + .../src/Plugin/rest/resource/Subscribe.php | 24 + .../src/Plugin/rest/resource/Unsubscribe.php | 24 + .../src/Plugin/rest/resource/UserInfo.php | 24 + .../src/Plugin/rest/resource/Wipeout.php | 24 + 7 files changed, 630 insertions(+), 436 deletions(-) create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php diff --git a/composer.json b/composer.json index a71a676..e558054 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "drupal/drupal_cms_starter": "~1.1.0", "drupal/project_browser": "@beta", "drupal/recipe_installer_kit": "^1-alpha3@alpha", + "drupal/restui": "^1.22", "drupal/webform": "@beta", "drush/drush": "^13" }, diff --git a/composer.lock b/composer.lock index fdeea01..5d72e0c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8c16dbf61bb5a2b4719cea2dac35115c", + "content-hash": "2bfc40d33cbaec3fb345421cc22b64f0", "packages": [ { "name": "asm89/stack-cors", @@ -1000,102 +1000,6 @@ }, "time": "2022-11-25T16:30:20+00:00" }, - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.1.2", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", - "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^2.2", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" - }, - "require-dev": { - "composer/composer": "^2.2", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcompatibility/php-compatibility": "^9.0", - "yoast/phpunit-polyfills": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Franck Nijhof", - "email": "opensource@frenck.dev", - "homepage": "https://frenck.dev", - "role": "Open source developer" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", - "source": "https://github.com/PHPCSStandards/composer-installer" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-07-17T20:45:56+00:00" - }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2571,57 +2475,6 @@ "issues": "https://www.drupal.org/project/issues/checklistapi" } }, - { - "name": "drupal/coder", - "version": "8.3.30", - "source": { - "type": "git", - "url": "https://github.com/pfrenssen/coder.git", - "reference": "6b2edffac77582b1beb36ac155bfda5e7e055aff" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pfrenssen/coder/zipball/6b2edffac77582b1beb36ac155bfda5e7e055aff", - "reference": "6b2edffac77582b1beb36ac155bfda5e7e055aff", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1 || ^1.0.0", - "ext-mbstring": "*", - "php": ">=7.2", - "sirbrillig/phpcs-variable-analysis": "^2.11.7", - "slevomat/coding-standard": "^8.11", - "squizlabs/php_codesniffer": "^3.13", - "symfony/yaml": ">=3.4.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.7.12", - "phpunit/phpunit": "^8.0" - }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "Drupal\\": "coder_sniffer/Drupal/", - "DrupalPractice\\": "coder_sniffer/DrupalPractice/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "description": "Coder is a library to review Drupal code.", - "homepage": "https://www.drupal.org/project/coder", - "keywords": [ - "code review", - "phpcs", - "standards" - ], - "support": { - "issues": "https://www.drupal.org/project/issues/coder", - "source": "https://www.drupal.org/project/coder" - }, - "time": "2025-05-25T09:52:20+00:00" - }, { "name": "drupal/coffee", "version": "2.0.1", @@ -6447,6 +6300,66 @@ "source": "https://git.drupalcode.org/project/redirect" } }, + { + "name": "drupal/restui", + "version": "1.22.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/restui.git", + "reference": "8.x-1.22" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/restui-8.x-1.22.zip", + "reference": "8.x-1.22", + "shasum": "7c9fb14c574f8a4090b77b1bcbc5be141409a383" + }, + "require": { + "drupal/core": "^9.5 || ^10 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-1.22", + "datestamp": "1721134189", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "-enzo-", + "homepage": "https://www.drupal.org/user/294937" + }, + { + "name": "clemens.tolboom", + "homepage": "https://www.drupal.org/user/125814" + }, + { + "name": "juampynr", + "homepage": "https://www.drupal.org/user/682736" + }, + { + "name": "kamkejj", + "homepage": "https://www.drupal.org/user/81043" + }, + { + "name": "vipin.mittal18", + "homepage": "https://www.drupal.org/user/319716" + } + ], + "description": "Provides a user interface to manage REST resources.", + "homepage": "https://www.drupal.org/project/restui", + "support": { + "source": "https://git.drupalcode.org/project/restui" + } + }, { "name": "drupal/robotstxt", "version": "1.6.0", @@ -10437,53 +10350,6 @@ }, "time": "2021-09-22T16:57:06+00:00" }, - { - "name": "phpstan/phpdoc-parser", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^5.3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^2.0", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" - }, - "time": "2025-07-13T07:04:09+00:00" - }, { "name": "psr/cache", "version": "3.0.0", @@ -11168,231 +11034,25 @@ "time": "2024-12-12T15:39:24+00:00" }, { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.12.0", + "name": "symfony/console", + "version": "v7.3.1", "source": { "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7" + "url": "https://github.com/symfony/console.git", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/4debf5383d9ade705e0a25121f16c3fecaf433a7", - "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", "shasum": "" }, "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" - }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" - }, - { - "name": "Payton Swick", - "email": "payton@foolord.com" - } - ], - "description": "A PHPCS sniff to detect problems with variables.", - "keywords": [ - "phpcs", - "static analysis" - ], - "support": { - "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", - "source": "https://github.com/sirbrillig/phpcs-variable-analysis", - "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" - }, - "time": "2025-03-17T16:17:38+00:00" - }, - { - "name": "slevomat/coding-standard", - "version": "8.19.1", - "source": { - "type": "git", - "url": "https://github.com/slevomat/coding-standard.git", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", - "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", - "php": "^7.4 || ^8.0", - "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.13.0" - }, - "require-dev": { - "phing/phing": "3.0.1", - "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.17", - "phpstan/phpstan-deprecation-rules": "2.0.3", - "phpstan/phpstan-phpunit": "2.0.6", - "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "8.x-dev" - } - }, - "autoload": { - "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", - "keywords": [ - "dev", - "phpcs" - ], - "support": { - "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" - }, - "funding": [ - { - "url": "https://github.com/kukulich", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", - "type": "tidelift" - } - ], - "time": "2025-06-09T17:53:57+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.13.2", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" - }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-06-17T22:17:01+00:00" - }, - { - "name": "symfony/console", - "version": "v7.3.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -14603,36 +14263,39 @@ "time": "2023-12-09T11:58:45+00:00" }, { - "name": "doctrine/instantiator", - "version": "2.0.0", + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.1.2", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", + "reference": "e9cf5e4bbf7eeaf9ef5db34938942602838fc2b1", "shasum": "" }, "require": { - "php": "^8.1" + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, - "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -14641,15 +14304,108 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", "keywords": [ - "constructor", + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-07-17T20:45:56+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", "instantiate" ], "support": { @@ -14672,6 +14428,57 @@ ], "time": "2022-12-30T00:23:10+00:00" }, + { + "name": "drupal/coder", + "version": "8.3.30", + "source": { + "type": "git", + "url": "https://github.com/pfrenssen/coder.git", + "reference": "6b2edffac77582b1beb36ac155bfda5e7e055aff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pfrenssen/coder/zipball/6b2edffac77582b1beb36ac155bfda5e7e055aff", + "reference": "6b2edffac77582b1beb36ac155bfda5e7e055aff", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1 || ^1.0.0", + "ext-mbstring": "*", + "php": ">=7.2", + "sirbrillig/phpcs-variable-analysis": "^2.11.7", + "slevomat/coding-standard": "^8.11", + "squizlabs/php_codesniffer": "^3.13", + "symfony/yaml": ">=3.4.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.7.12", + "phpunit/phpunit": "^8.0" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "Drupal\\": "coder_sniffer/Drupal/", + "DrupalPractice\\": "coder_sniffer/DrupalPractice/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Coder is a library to review Drupal code.", + "homepage": "https://www.drupal.org/project/coder", + "keywords": [ + "code review", + "phpcs", + "standards" + ], + "support": { + "issues": "https://www.drupal.org/project/issues/coder", + "source": "https://www.drupal.org/project/coder" + }, + "time": "2025-05-25T09:52:20+00:00" + }, { "name": "instaclick/php-webdriver", "version": "1.4.19", @@ -15265,6 +15072,53 @@ }, "time": "2025-05-13T13:52:32+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + }, + "time": "2025-07-13T07:04:09+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "11.0.10", @@ -16580,6 +16434,212 @@ ], "time": "2024-10-09T05:16:32+00:00" }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.12.0", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/4debf5383d9ade705e0a25121f16c3fecaf433a7", + "reference": "4debf5383d9ade705e0a25121f16c3fecaf433a7", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + }, + "time": "2025-03-17T16:17:38+00:00" + }, + { + "name": "slevomat/coding-standard", + "version": "8.19.1", + "source": { + "type": "git", + "url": "https://github.com/slevomat/coding-standard.git", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^2.1.0", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "require-dev": { + "phing/phing": "3.0.1", + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", + "phpstan/phpstan-phpunit": "2.0.6", + "phpstan/phpstan-strict-rules": "2.0.4", + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", + "keywords": [ + "dev", + "phpcs" + ], + "support": { + "issues": "https://github.com/slevomat/coding-standard/issues", + "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" + }, + "funding": [ + { + "url": "https://github.com/kukulich", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slevomat/coding-standard", + "type": "tidelift" + } + ], + "time": "2025-06-09T17:53:57+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-06-17T22:17:01+00:00" + }, { "name": "staabm/side-effects-detector", "version": "1.0.5", @@ -16833,5 +16893,5 @@ "prefer-lowest": false, "platform": {}, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php new file mode 100644 index 0000000..cfb3962 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php @@ -0,0 +1,37 @@ + "Ctools", + 'machine_name' => "ctools" + ], + [ + 'name' => "Meta tag", + 'machine_name' => "metatag" + ], + [ + 'name' => "Commerce", + 'machine_name' => "commerce" + ], + ]; + return new ResourceResponse($data); + } +} \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php new file mode 100644 index 0000000..4fab0f5 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -0,0 +1,24 @@ + NULL]; + return new ResourceResponse($data); + } +} \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php new file mode 100644 index 0000000..8d97126 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php @@ -0,0 +1,24 @@ + NULL]; + return new ResourceResponse($data); + } +} \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php new file mode 100644 index 0000000..e54dde6 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -0,0 +1,24 @@ + NULL]; + return new ResourceResponse($data); + } +} \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php new file mode 100644 index 0000000..ddaeb0c --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php @@ -0,0 +1,24 @@ + NULL]; + return new ResourceResponse($data); + } +} \ No newline at end of file From 35bc323c54b557c57567662e37cff378484eac9b Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 16 Mar 2026 14:54:20 +0200 Subject: [PATCH 05/57] Added modles needed for REST --- ...entity_form_display.node.ai_company.default.yml | 14 +++++++------- ...entity_view_display.node.ai_company.default.yml | 2 +- config/sync/core.extension.yml | 4 ++++ .../sync/core.menu.static_menu_link_overrides.yml | 4 ++-- ...ield.node.ai_company.field_company_ai_maker.yml | 1 + ...ode.ai_company.field_company_drupal_profile.yml | 1 + ...ode.ai_issue.field_issue_dashboard_category.yml | 1 + .../field.storage.node.field_company_ai_maker.yml | 1 + ...d.storage.node.field_company_drupal_profile.yml | 2 ++ ...storage.node.field_issue_dashboard_category.yml | 2 +- config/sync/views.view.ai_contributors_admin.yml | 11 +++-------- 11 files changed, 24 insertions(+), 19 deletions(-) diff --git a/config/sync/core.entity_form_display.node.ai_company.default.yml b/config/sync/core.entity_form_display.node.ai_company.default.yml index fcb5973..5f052e0 100644 --- a/config/sync/core.entity_form_display.node.ai_company.default.yml +++ b/config/sync/core.entity_form_display.node.ai_company.default.yml @@ -1,4 +1,4 @@ -uuid: null +uuid: 6967c42f-e7e0-4bd5-af2c-f13f1050622d langcode: en status: true dependencies: @@ -32,6 +32,12 @@ content: settings: display_label: true third_party_settings: { } + field_company_audience: + type: options_buttons + weight: 7 + region: content + settings: { } + third_party_settings: { } field_company_drupal_profile: type: string_textfield weight: 3 @@ -96,12 +102,6 @@ content: region: content settings: { } third_party_settings: { } - field_company_audience: - type: options_buttons - weight: 7 - region: content - settings: { } - third_party_settings: { } hidden: field_company_color: true field_company_logo: true diff --git a/config/sync/core.entity_view_display.node.ai_company.default.yml b/config/sync/core.entity_view_display.node.ai_company.default.yml index 3179582..7201e91 100644 --- a/config/sync/core.entity_view_display.node.ai_company.default.yml +++ b/config/sync/core.entity_view_display.node.ai_company.default.yml @@ -1,4 +1,4 @@ -uuid: null +uuid: ff2d9c37-c96d-4c60-8575-7df47b314cee langcode: en status: true dependencies: diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index ffad7fa..d52cc5f 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -94,9 +94,12 @@ module: redirect: 0 redirect_404: 0 responsive_image: 0 + rest: 0 + restui: 0 sam: 0 scheduler: 0 scheduler_content_moderation_integration: 0 + serialization: 0 smart_date: 0 svg_image: 0 symfony_mailer_lite: 0 @@ -111,6 +114,7 @@ module: update: 0 user: 0 views_ui: 0 + wisetrout_do_bot: 0 workflows: 0 eca_form: 1 pathauto: 1 diff --git a/config/sync/core.menu.static_menu_link_overrides.yml b/config/sync/core.menu.static_menu_link_overrides.yml index 7294a01..1b33d05 100644 --- a/config/sync/core.menu.static_menu_link_overrides.yml +++ b/config/sync/core.menu.static_menu_link_overrides.yml @@ -8,8 +8,8 @@ definitions: expanded: false enabled: false filter__tips_all: - enabled: true menu_name: tools parent: '' - expanded: false weight: 0 + expanded: false + enabled: true diff --git a/config/sync/field.field.node.ai_company.field_company_ai_maker.yml b/config/sync/field.field.node.ai_company.field_company_ai_maker.yml index 7036cd4..03515e3 100644 --- a/config/sync/field.field.node.ai_company.field_company_ai_maker.yml +++ b/config/sync/field.field.node.ai_company.field_company_ai_maker.yml @@ -1,3 +1,4 @@ +uuid: bf85eeb1-a348-42d1-a77f-cc59a328ff9c langcode: en status: true dependencies: diff --git a/config/sync/field.field.node.ai_company.field_company_drupal_profile.yml b/config/sync/field.field.node.ai_company.field_company_drupal_profile.yml index c4db7c7..5da7654 100644 --- a/config/sync/field.field.node.ai_company.field_company_drupal_profile.yml +++ b/config/sync/field.field.node.ai_company.field_company_drupal_profile.yml @@ -1,3 +1,4 @@ +uuid: e956d50d-813a-4fcc-a2a3-a7673f74c4b4 langcode: en status: true dependencies: diff --git a/config/sync/field.field.node.ai_issue.field_issue_dashboard_category.yml b/config/sync/field.field.node.ai_issue.field_issue_dashboard_category.yml index 7d14aeb..84e8577 100644 --- a/config/sync/field.field.node.ai_issue.field_issue_dashboard_category.yml +++ b/config/sync/field.field.node.ai_issue.field_issue_dashboard_category.yml @@ -1,3 +1,4 @@ +uuid: 4461182d-8822-4af8-a1a4-acdb0019ec53 langcode: en status: true dependencies: diff --git a/config/sync/field.storage.node.field_company_ai_maker.yml b/config/sync/field.storage.node.field_company_ai_maker.yml index a0ad38d..821b3b9 100644 --- a/config/sync/field.storage.node.field_company_ai_maker.yml +++ b/config/sync/field.storage.node.field_company_ai_maker.yml @@ -1,3 +1,4 @@ +uuid: f044f7e8-6f1c-4ff7-b95b-6a40f872d40c langcode: en status: true dependencies: diff --git a/config/sync/field.storage.node.field_company_drupal_profile.yml b/config/sync/field.storage.node.field_company_drupal_profile.yml index 1d6c836..3260d8f 100644 --- a/config/sync/field.storage.node.field_company_drupal_profile.yml +++ b/config/sync/field.storage.node.field_company_drupal_profile.yml @@ -1,3 +1,4 @@ +uuid: d229c08e-d43a-4278-a86b-ff86760f7eb0 langcode: en status: true dependencies: @@ -10,6 +11,7 @@ type: string settings: max_length: 255 case_sensitive: false + is_ascii: false module: core locked: false cardinality: 1 diff --git a/config/sync/field.storage.node.field_issue_dashboard_category.yml b/config/sync/field.storage.node.field_issue_dashboard_category.yml index e858ade..c876c29 100644 --- a/config/sync/field.storage.node.field_issue_dashboard_category.yml +++ b/config/sync/field.storage.node.field_issue_dashboard_category.yml @@ -1,4 +1,4 @@ -uuid: null +uuid: 9f9e584d-cea3-4f81-b34e-d828f3e4efa0 langcode: en status: true dependencies: diff --git a/config/sync/views.view.ai_contributors_admin.yml b/config/sync/views.view.ai_contributors_admin.yml index a72af45..7410bba 100644 --- a/config/sync/views.view.ai_contributors_admin.yml +++ b/config/sync/views.view.ai_contributors_admin.yml @@ -1,3 +1,4 @@ +uuid: e4738277-0262-4d7d-b763-8af9058d83f9 langcode: en status: true dependencies: @@ -468,9 +469,6 @@ display: multiple: false remember_roles: authenticated: authenticated - anonymous: '0' - content_editor: '0' - administrator: '0' is_grouped: false group_info: label: '' @@ -509,9 +507,6 @@ display: multiple: false remember_roles: authenticated: authenticated - anonymous: '0' - content_editor: '0' - administrator: '0' is_grouped: false group_info: label: '' @@ -617,11 +612,11 @@ display: type: none title: 'AI Contributors' description: '' + weight: 0 expanded: false + menu_name: main parent: '' - weight: 0 context: '0' - menu_name: main cache_metadata: max-age: -1 contexts: From be960ed8c61fb0cc5ff9ae64ef317ad6cc20c457 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 19 Mar 2026 12:37:48 +0200 Subject: [PATCH 06/57] Added Simple Oauth module --- .gitignore | 2 + composer.json | 1 + composer.lock | 965 +++++++++++++++++- config/sync/core.extension.yml | 2 + ...oauth.oauth2_token.bundle.access_token.yml | 10 + ...le_oauth.oauth2_token.bundle.auth_code.yml | 10 + ...auth.oauth2_token.bundle.refresh_token.yml | 10 + config/sync/simple_oauth.settings.yml | 5 + ...tion.user_add_role_action.telegram_bot.yml | 14 + ...n.user_remove_role_action.telegram_bot.yml | 14 + config/sync/user.role.telegram_bot.yml | 9 + 11 files changed, 1019 insertions(+), 23 deletions(-) create mode 100644 config/sync/simple_oauth.oauth2_token.bundle.access_token.yml create mode 100644 config/sync/simple_oauth.oauth2_token.bundle.auth_code.yml create mode 100644 config/sync/simple_oauth.oauth2_token.bundle.refresh_token.yml create mode 100644 config/sync/simple_oauth.settings.yml create mode 100644 config/sync/system.action.user_add_role_action.telegram_bot.yml create mode 100644 config/sync/system.action.user_remove_role_action.telegram_bot.yml create mode 100644 config/sync/user.role.telegram_bot.yml diff --git a/.gitignore b/.gitignore index ad896fa..ba2c594 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,5 @@ test-kanban-login.js # Ignore local development code scratchpad .code/ + +private \ No newline at end of file diff --git a/composer.json b/composer.json index e558054..dd5f99d 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "drupal/project_browser": "@beta", "drupal/recipe_installer_kit": "^1-alpha3@alpha", "drupal/restui": "^1.22", + "drupal/simple_oauth": "^6.1", "drupal/webform": "@beta", "drush/drush": "^13" }, diff --git a/composer.lock b/composer.lock index 5d72e0c..6160d61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2bfc40d33cbaec3fb345421cc22b64f0", + "content-hash": "0f203bbc55635088f13b728769ae9430", "packages": [ { "name": "asm89/stack-cors", @@ -1000,6 +1000,73 @@ }, "time": "2022-11-25T16:30:20+00:00" }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2608,6 +2675,58 @@ "issues": "http://drupal.org/project/config_ignore" } }, + { + "name": "drupal/consumers", + "version": "1.24.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/consumers.git", + "reference": "8.x-1.24" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/consumers-8.x-1.24.zip", + "reference": "8.x-1.24", + "shasum": "63acc012badaec2dd15cb4dd3e9cab9cb8a83eef" + }, + "require": { + "drupal/core": "^10.3 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-1.24", + "datestamp": "1770824862", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "bojan_dev", + "homepage": "https://www.drupal.org/user/2801849" + }, + { + "name": "e0ipso", + "homepage": "https://www.drupal.org/user/550110" + }, + { + "name": "eojthebrave", + "homepage": "https://www.drupal.org/user/79230" + } + ], + "description": "Declare all the consumers of your API.", + "homepage": "https://www.drupal.org/project/consumers", + "support": { + "source": "https://git.drupalcode.org/project/consumers" + } + }, { "name": "drupal/core", "version": "11.2.2", @@ -6929,6 +7048,75 @@ "issues": "https://www.drupal.org/project/issues/seo_checklist" } }, + { + "name": "drupal/simple_oauth", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/simple_oauth.git", + "reference": "6.1.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/simple_oauth-6.1.0.zip", + "reference": "6.1.0", + "shasum": "3c345e42cf41a17b2922b0b69129ac2697cf566a" + }, + "require": { + "drupal/consumers": "^1.17", + "drupal/core": "^10.3 || ^11", + "league/oauth2-server": "^9.0", + "php": ">=8.1", + "steverhoades/oauth2-openid-connect-server": "^3.0" + }, + "require-dev": { + "drupal/simple_oauth_static_scope": "*", + "phpspec/prophecy-phpunit": "^2" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "6.1.0", + "datestamp": "1765191669", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Mateu AguilΓ³ Bosch", + "homepage": "https://www.drupal.org/user/2801849", + "email": "mateu.aguilo.bosch@gmail.com" + }, + { + "name": "bradjones1", + "homepage": "https://www.drupal.org/user/405824" + }, + { + "name": "e0ipso", + "homepage": "https://www.drupal.org/user/550110" + }, + { + "name": "kingdutch", + "homepage": "https://www.drupal.org/user/1868952" + }, + { + "name": "pcambra", + "homepage": "https://www.drupal.org/user/122101" + } + ], + "description": "The Simple OAuth module for Drupal", + "homepage": "https://www.drupal.org/project/simple_oauth", + "support": { + "source": "https://git.drupalcode.org/project/simple_oauth" + } + }, { "name": "drupal/simple_search_form", "version": "1.6.0", @@ -8694,6 +8882,143 @@ }, "time": "2025-07-07T14:17:42+00:00" }, + { + "name": "lcobucci/clock", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/a3139d9e97d47826f27e6a17bb63f13621f86058", + "reference": "a3139d9e97d47826f27e6a17bb63f13621f86058", + "shasum": "" + }, + "require": { + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.31", + "lcobucci/coding-standard": "^11.2.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^2.0.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0.0", + "phpstan/phpstan-strict-rules": "^2.0.0", + "phpunit/phpunit": "^12.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "LuΓ­s Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.5.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-10-27T09:03:17+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.6.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/bb3e9f21e4196e8afc41def81ef649c164bca25e", + "reference": "bb3e9f21e4196e8afc41def81ef649c164bca25e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "LuΓ­s Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.6.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-10-17T11:30:53+00:00" + }, { "name": "league/commonmark", "version": "2.7.1", @@ -8965,6 +9290,65 @@ ], "time": "2025-05-20T12:55:37+00:00" }, + { + "name": "league/event", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "shasum": "" + }, + "require": { + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.3" + }, + "time": "2024-09-04T16:06:53+00:00" + }, { "name": "league/html-to-markdown", "version": "5.1.1", @@ -9055,35 +9439,52 @@ "time": "2023-07-12T21:21:09+00:00" }, { - "name": "masterminds/html5", - "version": "2.9.0", + "name": "league/oauth2-server", + "version": "9.3.0", "source": { "type": "git", - "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", - "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/d8e2f39f645a82b207bbac441694d6e6079357cb", + "reference": "d8e2f39f645a82b207bbac441694d6e6079357cb", "shasum": "" }, "require": { - "ext-dom": "*", - "php": ">=5.3.0" + "defuse/php-encryption": "^2.4", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.3 || ^3.0", + "lcobucci/jwt": "^5.0", + "league/event": "^3.0", + "league/uri": "^7.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-message": "^2.0", + "psr/http-server-middleware": "^1.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + "laminas/laminas-diactoros": "^3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.12|^2.0", + "phpstan/phpstan-deprecation-rules": "^1.1.4|^2.0", + "phpstan/phpstan-phpunit": "^1.3.15|^2.0", + "phpstan/phpstan-strict-rules": "^1.5.2|^2.0", + "phpunit/phpunit": "^10.5|^11.5|^12.0", + "roave/security-advisories": "dev-master", + "slevomat/coding-standard": "^8.14.1", + "squizlabs/php_codesniffer": "^3.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, "autoload": { "psr-4": { - "Masterminds\\": "src" + "League\\OAuth2\\Server\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -9092,15 +9493,276 @@ ], "authors": [ { - "name": "Matt Butcher", - "email": "technosophos@gmail.com" - }, - { - "name": "Matt Farina", - "email": "matt@mattfarina.com" + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" }, { - "name": "Asmir Mustafic", + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/9.3.0" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2025-11-25T22:51:15+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", "email": "goetas@gmail.com" } ], @@ -9520,6 +10182,56 @@ ], "time": "2025-06-24T10:49:48+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "pear/archive_tar", "version": "1.5.0", @@ -10399,6 +11111,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -10662,6 +11422,119 @@ }, "time": "2023-04-04T09:54:51+00:00" }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -11033,6 +11906,52 @@ }, "time": "2024-12-12T15:39:24+00:00" }, + { + "name": "steverhoades/oauth2-openid-connect-server", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/steverhoades/oauth2-openid-connect-server.git", + "reference": "f20665aeeeec78fb336c1bc89e8152b4b2380221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/steverhoades/oauth2-openid-connect-server/zipball/f20665aeeeec78fb336c1bc89e8152b4b2380221", + "reference": "f20665aeeeec78fb336c1bc89e8152b4b2380221", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "4.1.5|^4.2|^4.3|^5.0", + "league/oauth2-server": "^8.4.2|^9.0", + "php": ">=7.4" + }, + "require-dev": { + "laminas/laminas-diactoros": "^1.3.2 || ^3.3", + "phpunit/phpunit": "^5.0|^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "OpenIDConnectServer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Rhoades", + "email": "sedonami@gmail.com" + } + ], + "description": "An OpenID Connect Server that sites on The PHP League's OAuth2 Server", + "support": { + "issues": "https://github.com/steverhoades/oauth2-openid-connect-server/issues", + "source": "https://github.com/steverhoades/oauth2-openid-connect-server/tree/v3.0.1" + }, + "time": "2024-09-26T16:19:13+00:00" + }, { "name": "symfony/console", "version": "v7.3.1", diff --git a/config/sync/core.extension.yml b/config/sync/core.extension.yml index d52cc5f..1780b2e 100644 --- a/config/sync/core.extension.yml +++ b/config/sync/core.extension.yml @@ -28,6 +28,7 @@ module: coffee: 0 config: 0 config_ignore: 0 + consumers: 0 content_moderation: 0 contextual: 0 crop: 0 @@ -100,6 +101,7 @@ module: scheduler: 0 scheduler_content_moderation_integration: 0 serialization: 0 + simple_oauth: 0 smart_date: 0 svg_image: 0 symfony_mailer_lite: 0 diff --git a/config/sync/simple_oauth.oauth2_token.bundle.access_token.yml b/config/sync/simple_oauth.oauth2_token.bundle.access_token.yml new file mode 100644 index 0000000..933cbc2 --- /dev/null +++ b/config/sync/simple_oauth.oauth2_token.bundle.access_token.yml @@ -0,0 +1,10 @@ +uuid: da136a35-4129-4977-93d0-fee85c78a8b2 +langcode: en +status: true +dependencies: { } +_core: + default_config_hash: f7Fq1Sh24P1luZf4RRdeDs4be4diwuU5P-XkpuxjyLk +id: access_token +label: 'Access Token' +description: 'The access token type.' +locked: true diff --git a/config/sync/simple_oauth.oauth2_token.bundle.auth_code.yml b/config/sync/simple_oauth.oauth2_token.bundle.auth_code.yml new file mode 100644 index 0000000..4353adf --- /dev/null +++ b/config/sync/simple_oauth.oauth2_token.bundle.auth_code.yml @@ -0,0 +1,10 @@ +uuid: 4ac210a0-9ca1-4b83-bbef-a70e95efdc12 +langcode: en +status: true +dependencies: { } +_core: + default_config_hash: zlxpI1nUDS46lrOkyHiD-72D5OFvlADLiyXT5B1iErk +id: auth_code +label: 'Auth code' +description: 'The auth code type.' +locked: true diff --git a/config/sync/simple_oauth.oauth2_token.bundle.refresh_token.yml b/config/sync/simple_oauth.oauth2_token.bundle.refresh_token.yml new file mode 100644 index 0000000..eb9accb --- /dev/null +++ b/config/sync/simple_oauth.oauth2_token.bundle.refresh_token.yml @@ -0,0 +1,10 @@ +uuid: e46df633-5013-4ae0-9471-e604981f668e +langcode: en +status: true +dependencies: { } +_core: + default_config_hash: dTxawscb7hlymHgGQJP1DO58uotCWthP68yXbmv6LL8 +id: refresh_token +label: 'Refresh token' +description: 'The refresh token type.' +locked: true diff --git a/config/sync/simple_oauth.settings.yml b/config/sync/simple_oauth.settings.yml new file mode 100644 index 0000000..06b4a50 --- /dev/null +++ b/config/sync/simple_oauth.settings.yml @@ -0,0 +1,5 @@ +_core: + default_config_hash: ci56aVE-Jo4w_Xy5Gkk1coAwO-fj7Q87l0PKc5E9sfo +scope_provider: dynamic +token_cron_batch_size: 0 +disable_openid_connect: false diff --git a/config/sync/system.action.user_add_role_action.telegram_bot.yml b/config/sync/system.action.user_add_role_action.telegram_bot.yml new file mode 100644 index 0000000..f5ad4b9 --- /dev/null +++ b/config/sync/system.action.user_add_role_action.telegram_bot.yml @@ -0,0 +1,14 @@ +uuid: f23eae79-6b16-43bf-ad3b-b83a5f456195 +langcode: en +status: true +dependencies: + config: + - user.role.telegram_bot + module: + - user +id: user_add_role_action.telegram_bot +label: 'Add the Telegram bot role to the selected user(s)' +type: user +plugin: user_add_role_action +configuration: + rid: telegram_bot diff --git a/config/sync/system.action.user_remove_role_action.telegram_bot.yml b/config/sync/system.action.user_remove_role_action.telegram_bot.yml new file mode 100644 index 0000000..4a8eb74 --- /dev/null +++ b/config/sync/system.action.user_remove_role_action.telegram_bot.yml @@ -0,0 +1,14 @@ +uuid: 3ff75386-1efc-4a9a-a519-4ebdd9214266 +langcode: en +status: true +dependencies: + config: + - user.role.telegram_bot + module: + - user +id: user_remove_role_action.telegram_bot +label: 'Remove the Telegram bot role from the selected user(s)' +type: user +plugin: user_remove_role_action +configuration: + rid: telegram_bot diff --git a/config/sync/user.role.telegram_bot.yml b/config/sync/user.role.telegram_bot.yml new file mode 100644 index 0000000..48bcdba --- /dev/null +++ b/config/sync/user.role.telegram_bot.yml @@ -0,0 +1,9 @@ +uuid: ee20a0d5-2658-4a72-bfb5-4d6931951d7a +langcode: en +status: true +dependencies: { } +id: telegram_bot +label: 'Telegram bot' +weight: 4 +is_admin: false +permissions: { } From f7ee34010071c4ab32230cd3dc0e5b0a0b3f0a35 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 19 Mar 2026 12:38:24 +0200 Subject: [PATCH 07/57] Added Telegram bot to ddev --- .ddev/docker-compose.wt-do-bot.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .ddev/docker-compose.wt-do-bot.yaml diff --git a/.ddev/docker-compose.wt-do-bot.yaml b/.ddev/docker-compose.wt-do-bot.yaml new file mode 100644 index 0000000..d75a8b4 --- /dev/null +++ b/.ddev/docker-compose.wt-do-bot.yaml @@ -0,0 +1,11 @@ +services: + do-bot: + build: + context: ../wisetrout-do-bot + networks: ["default", "ddev_default"] + env_file: ../wisetrout-do-bot/.env + volumes: + - type: bind + source: ../wisetrout-do-bot + target: /app + # command: "sh -c npm install && npm start" From acc0a6c3f9162d86f1e88024d8ec0a354f932af5 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 25 Mar 2026 10:28:54 +0200 Subject: [PATCH 08/57] feat: acquiring OAuth token by Telegram bot --- wisetrout-do-bot/api-calls/oauth.js | 31 +++++++++ wisetrout-do-bot/bot.js | 103 +++++++++++++++------------- wisetrout-do-bot/oauth.js | 18 +++++ 3 files changed, 106 insertions(+), 46 deletions(-) create mode 100644 wisetrout-do-bot/api-calls/oauth.js create mode 100644 wisetrout-do-bot/oauth.js diff --git a/wisetrout-do-bot/api-calls/oauth.js b/wisetrout-do-bot/api-calls/oauth.js new file mode 100644 index 0000000..452dcd0 --- /dev/null +++ b/wisetrout-do-bot/api-calls/oauth.js @@ -0,0 +1,31 @@ +export async function getOAuthToken(){ + + + const data = { + grant_type: process.env.DRUPAL_AUTH_TOKEN_GRANT_TYPE || '', + client_id: process.env.DRUPAL_AUTH_TOKEN_CLIENT_ID || '', + client_secret: process.env.DRUPAL_AUTH_TOKEN_CLIENT_SECRET || '', + scope: process.env.DRUPAL_AUTH_TOKEN_SCOPE || '', + }; + + const body = new URLSearchParams(data); + + try{ + const res = await fetch(process.env.BASE_URL + '/oauth/token', { + method: 'POST', + body: body + }) + + if(!res.ok) throw new Error('Request issue'); + + const json = await res.json(); + + const token = json.access_token; + + return token; + + }catch(err){ + console.log(err); + return null; + } +} \ No newline at end of file diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 06e1c6d..95e8de5 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -5,65 +5,76 @@ import { createActionsKeyboard } from './markup/keyboards.js'; import { scene as wipeoutScene } from './scenes/wipeout.js'; import { subscribe, unsubscribe } from './api-calls/subscription.js'; import { Stage } from 'telegraf/scenes'; +import { activateOAuthToken } from './oauth.js'; -const bot = new Telegraf(process.env.BOT_TOKEN); +activateOAuthToken() +.then(() => { + activateBot(); +}); -bot.use(session()); -const stage = new Stage([subscrScene, wipeoutScene]); -bot.use(preferencesMiddleware); -bot.use(stage.middleware()); +function activateBot(){ + + const bot = new Telegraf(process.env.BOT_TOKEN); -bot.start(ctx => { - ctx.reply( - 'Welcome to the Wisetrout Drupal org bot!', - createActionsKeyboard(ctx) - ) -}); + bot.use(session()); -bot.action('sub', ctx => { - ctx.scene.enter('subscription-menu'); -}) + const stage = new Stage([subscrScene, wipeoutScene]); -bot.action('unsub', async ctx => { - await Promise.all([ - ctx.reply('Cancelling your subscription...'), - unsubscribe(ctx.from.id) - ]); + bot.use(preferencesMiddleware); + bot.use(stage.middleware()); - ctx.session.userInfo.subscribed = false; - ctx.reply('Unsubscribed from all updates. We will keep your preferences saved in case you want to renew your subscription.', - createActionsKeyboard(ctx) - ); -}); + bot.start(ctx => { + ctx.reply( + 'Welcome to the Wisetrout Drupal org bot!', + createActionsKeyboard(ctx) + ) + }); -bot.action('resub', async ctx => { + bot.action('sub', ctx => { + ctx.scene.enter('subscription-menu'); + }) - await Promise.all([ - ctx.reply('Reactivating your subscription...'), - subscribe(ctx.from.id, ctx.session.userInfo.modules) - ]); + bot.action('unsub', async ctx => { + await Promise.all([ + ctx.reply('Cancelling your subscription...'), + unsubscribe(ctx.from.id) + ]); - ctx.session.userInfo.subscribed = true; - ctx.reply('Subscription reactivated', - createActionsKeyboard(ctx) - ); + ctx.session.userInfo.subscribed = false; + ctx.reply('Unsubscribed from all updates. We will keep your preferences saved in case you want to renew your subscription.', + createActionsKeyboard(ctx) + ); + }); -}) + bot.action('resub', async ctx => { -bot.action('wipeout', async ctx => { - ctx.scene.enter('wipeout'); -}); + await Promise.all([ + ctx.reply('Reactivating your subscription...'), + subscribe(ctx.from.id, ctx.session.userInfo.modules) + ]); + + ctx.session.userInfo.subscribed = true; + ctx.reply('Subscription reactivated', + createActionsKeyboard(ctx) + ); + + }) + + bot.action('wipeout', async ctx => { + ctx.scene.enter('wipeout'); + }); -// bot.action('help', async ctx => { -// ctx.reply('You may use the following commands to manage your subscription:', -// createActionsKeyboard(ctx) -// ); -// }); + // bot.action('help', async ctx => { + // ctx.reply('You may use the following commands to manage your subscription:', + // createActionsKeyboard(ctx) + // ); + // }); -bot.launch(); + bot.launch(); -// Enable graceful stop -process.once('SIGINT', () => bot.stop('SIGINT')) -process.once('SIGTERM', () => bot.stop('SIGTERM')) \ No newline at end of file + // Enable graceful stop + process.once('SIGINT', () => bot.stop('SIGINT')) + process.once('SIGTERM', () => bot.stop('SIGTERM')) +} diff --git a/wisetrout-do-bot/oauth.js b/wisetrout-do-bot/oauth.js new file mode 100644 index 0000000..4d59b77 --- /dev/null +++ b/wisetrout-do-bot/oauth.js @@ -0,0 +1,18 @@ + +const TOKEN_EXPIRATION_MS = 5 * 60 * 1000; + +import { getOAuthToken } from "./api-calls/oauth.js"; + +export let oAuthToken = null; + +export async function activateOAuthToken(){ + + oAuthToken = await getOAuthToken(); + + setInterval(async function(){ + oAuthToken = await getOAuthToken(); + }, TOKEN_EXPIRATION_MS); + + return oAuthToken; + +} \ No newline at end of file From 5697d4e19a614cc8176e4f5ddec6e9e4a5d280e6 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 26 Mar 2026 12:30:00 +0200 Subject: [PATCH 09/57] feat: using oAuth to access modules list --- wisetrout-do-bot/api-calls/modules-list.js | 34 +++++++------------- wisetrout-do-bot/api-calls/oauth.js | 2 +- wisetrout-do-bot/bot.js | 6 +++- wisetrout-do-bot/scenes/subscription-menu.js | 28 +++++++++------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/wisetrout-do-bot/api-calls/modules-list.js b/wisetrout-do-bot/api-calls/modules-list.js index 5363cee..f0f2e72 100644 --- a/wisetrout-do-bot/api-calls/modules-list.js +++ b/wisetrout-do-bot/api-calls/modules-list.js @@ -1,24 +1,14 @@ +import { oAuthToken } from "../oauth"; + export async function getModulesList(){ - return [ - { - name: "Ctools", - machine_name: "ctools" - }, - { - name: "Meta tag", - machine_name: "metatag" - }, - { - name: "Commerce", - machine_name: "commerce" - }, - { - name: "AI", - machine_name: "ai" - }, - { - name: "Facebook", - machine_name: "facebook" - } - ] + const response = await fetch(process.env.BASE_URL + '/api/telegram/modules-list', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${oAuthToken}` + } + }) + + if(!response.ok) throw new Error(response.status) + + return await response.json(); } \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/oauth.js b/wisetrout-do-bot/api-calls/oauth.js index 452dcd0..674e0a6 100644 --- a/wisetrout-do-bot/api-calls/oauth.js +++ b/wisetrout-do-bot/api-calls/oauth.js @@ -16,7 +16,7 @@ export async function getOAuthToken(){ body: body }) - if(!res.ok) throw new Error('Request issue'); + if(!res.ok) throw new Error(res.status); const json = await res.json(); diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 95e8de5..3cd5266 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -5,7 +5,7 @@ import { createActionsKeyboard } from './markup/keyboards.js'; import { scene as wipeoutScene } from './scenes/wipeout.js'; import { subscribe, unsubscribe } from './api-calls/subscription.js'; import { Stage } from 'telegraf/scenes'; -import { activateOAuthToken } from './oauth.js'; +import { activateOAuthToken, oAuthToken } from './oauth.js'; activateOAuthToken() .then(() => { @@ -26,6 +26,10 @@ function activateBot(){ bot.use(stage.middleware()); bot.start(ctx => { + + ctx.reply(oAuthToken ? 'Token created' : 'Could not create token') + + ctx.reply( 'Welcome to the Wisetrout Drupal org bot!', createActionsKeyboard(ctx) diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 16edecb..023d56f 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -33,18 +33,24 @@ scene.action("all", async ctx => { scene.action("specific", async ctx => { - const [modules] = await Promise.all([ - getModulesList(), - ctx.reply("Loading...") - ]) - ; - - ctx.session.modules = modules; - ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; + try{ + const [modules] = await Promise.all([ + getModulesList(), + ctx.reply("Loading...") + ]); + + ctx.session.modules = modules; + ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; + + await ctx.reply('Please pick the modules you are interested in and click "subscribe".', + Markup.inlineKeyboard(createKeyboardRows(ctx)) + ); + }catch(err){ + ctx.reply('Error'); + ctx.reply(err); + } - await ctx.reply('Please pick the modules you are interested in and click "subscribe".', - Markup.inlineKeyboard(createKeyboardRows(ctx)) - ); + }) scene.action("complete", async ctx => { From ef1380c97676b494c09037b05453c134dcbb0b90 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 30 Mar 2026 12:38:24 +0300 Subject: [PATCH 10/57] feat: real endpoint for fetching modules list --- .../src/Plugin/rest/resource/ModulesList.php | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php index cfb3962..8e02a72 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php @@ -2,6 +2,7 @@ namespace Drupal\wisetrout_do_bot\Plugin\rest\resource; + use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; @@ -18,20 +19,14 @@ */ class ModulesList extends ResourceBase { public function get() { - $data = [ - [ - 'name' => "Ctools", - 'machine_name' => "ctools" - ], - [ - 'name' => "Meta tag", - 'machine_name' => "metatag" - ], - [ - 'name' => "Commerce", - 'machine_name' => "commerce" - ], - ]; - return new ResourceResponse($data); + + $nids = \Drupal::entityQuery('node') + ->condition('type', 'ai_module') + ->accessCheck(FALSE) + ->execute(); + + $nodes = \Drupal\node\Entity\node::loadMultiple($nids); + + return new ResourceResponse($nodes); } } \ No newline at end of file From 2a5c79c877bd69bad7182cb34f5beebf2f98a174 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 30 Mar 2026 13:11:30 +0300 Subject: [PATCH 11/57] feat: subscription menu in Telegram bot with real modules data --- wisetrout-do-bot/api-calls/modules-list.js | 22 ++- wisetrout-do-bot/bot.js | 10 +- wisetrout-do-bot/scenes/subscription-menu.js | 170 ++++++++++++++----- 3 files changed, 146 insertions(+), 56 deletions(-) diff --git a/wisetrout-do-bot/api-calls/modules-list.js b/wisetrout-do-bot/api-calls/modules-list.js index f0f2e72..060bd2f 100644 --- a/wisetrout-do-bot/api-calls/modules-list.js +++ b/wisetrout-do-bot/api-calls/modules-list.js @@ -1,4 +1,4 @@ -import { oAuthToken } from "../oauth"; +import { oAuthToken } from "../oauth.js"; export async function getModulesList(){ const response = await fetch(process.env.BASE_URL + '/api/telegram/modules-list', { @@ -8,7 +8,21 @@ export async function getModulesList(){ } }) - if(!response.ok) throw new Error(response.status) + if(!response.ok) throw new Error(response.status); + + const resObj = await response.json(); + + const modules = []; + for(const id in resObj){ + + const { field_module_machine_name } = resObj[id]; + + modules.push(field_module_machine_name[0].value); + } + + modules.sort(); + + + return modules; +} - return await response.json(); -} \ No newline at end of file diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 3cd5266..4093ce8 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -70,11 +70,11 @@ function activateBot(){ ctx.scene.enter('wipeout'); }); - // bot.action('help', async ctx => { - // ctx.reply('You may use the following commands to manage your subscription:', - // createActionsKeyboard(ctx) - // ); - // }); + bot.help(async ctx => { + ctx.reply('You may use the following commands to manage your subscription:', + createActionsKeyboard(ctx) + ); + }); bot.launch(); diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 023d56f..04ece04 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -2,6 +2,8 @@ import { Markup, Scenes } from "telegraf"; import { getModulesList } from "../api-calls/modules-list.js"; import { subscribe } from "../api-calls/subscription.js"; +const MODULES_PER_PAGE = 10; + export const scene = new Scenes.BaseScene('subscription-menu'); scene.enter(async ctx => { @@ -25,7 +27,7 @@ scene.action("all", async ctx => { ctx.session.userInfo = { subscribed: true, - modules: [] + modules: null } ctx.reply('Subscription successful!'); ctx.scene.leave(); @@ -38,13 +40,21 @@ scene.action("specific", async ctx => { getModulesList(), ctx.reply("Loading...") ]); + ctx.session.modules = modules; ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; + ctx.session.page = 0; + + await ctx.reply('Please pick the modules you are interested in and click "subscribe"'); + + const messageData = await ctx.reply(createModulesList(ctx), Markup.inlineKeyboard(createMenuRows(ctx))); - await ctx.reply('Please pick the modules you are interested in and click "subscribe".', - Markup.inlineKeyboard(createKeyboardRows(ctx)) - ); + const { message_id } = messageData; + + + ctx.session.menuMessageId = message_id; + }catch(err){ ctx.reply('Error'); ctx.reply(err); @@ -53,10 +63,57 @@ scene.action("specific", async ctx => { }) -scene.action("complete", async ctx => { +scene.action("page_back", ctx => { + ctx.session.page--; + refreshMenu(ctx); + +}) + +scene.action("page_next", ctx => { + ctx.session.page++; + refreshMenu(ctx); +}) + +scene.command(/.+/, async ctx => { + const { command } = ctx; + console.log('Command:'); + console.log(command); + + + if(['page_back', 'page_next', 'complete', 'cancel', 'all', 'specific', ''].includes(command)) return; + + + if(ctx.session.selectedModules.includes(command)){ + ctx.session.selectedModules = ctx.session.selectedModules.filter(m => m != command); + }else{ + ctx.session.selectedModules.push(command); + } + + refreshMenu(ctx); + + ctx.deleteMessage(); +}) + +scene.action("select_all", async ctx => { + ctx.session.selectedModules = [...ctx.session.modules]; + + refreshMenu(ctx); + +}) + +scene.action('select_none', ctx => { + ctx.session.selectedModules = []; + refreshMenu(ctx); +}) + + +scene.action("save", async ctx => { + const modulesToSubscribe = ctx.session.modules.length === ctx.session.selectedModules.length ? + null : + ctx.session.selectedModules; await Promise.all([ - ctx.reply('Subscribing to updates...'), - subscribe(ctx.from.id, ctx.session.selectedModules) + ctx.reply('Subscribing to updates..'), + subscribe(ctx.from.id, modulesToSubscribe) ]); ctx.session.userInfo = { subscribed: true, @@ -64,6 +121,7 @@ scene.action("complete", async ctx => { } delete ctx.session.selectedModule; delete ctx.session.modules; + delete ctx.session.menuMessageId; ctx.reply('Subscription successful!'); ctx.scene.leave(); }) @@ -74,49 +132,67 @@ scene.action("cancel", ctx => { ctx.reply("Subscription process cancelled"); if(ctx.session.selectedModules) delete ctx.session.selectedModules; if(ctx.session.modules) delete ctx.session.modules; + delete ctx.session.menuMessageId; ctx.scene.leave(); }); -scene.on("callback_query", async ctx => { - const [prefix, action, moduleMachineName] = ctx.callbackQuery.data.split('--'); - if(prefix != "module") return; - - if(action === "select"){ - ctx.session.selectedModules.push(moduleMachineName); - }else{ - ctx.session.selectedModules = ctx.session.selectedModules.filter(mn => mn != moduleMachineName); - } - ctx.editMessageReplyMarkup( - { - inline_keyboard: createKeyboardRows(ctx) - } - ); -}); - - -function createKeyboardRows(ctx){ - const keyboardRows = []; - - const {modules, selectedModules} = ctx.session; - - modules.forEach((module, moduleIndex) => { - - const {name, machine_name} = module; - const moduleSelected = selectedModules.includes(machine_name); - const btnText = `${moduleSelected ? "βœ”οΈ ":""}${name}`; - const btnAction = `module--${moduleSelected ? "deselect":"select"}--${machine_name}`; - - const btn = Markup.button.callback(btnText, btnAction); - - if(moduleIndex % 3 === 0){ - keyboardRows.push([btn]); - }else{ - keyboardRows[keyboardRows.length - 1].push(btn); +function createModulesList(ctx){ + let message = ''; + const firstModuleIndex = ctx.session.page * MODULES_PER_PAGE; + const lastModuleIndex = (ctx.session.page + 1) * MODULES_PER_PAGE; + + + const modulesOnPage = ctx.session.modules + .slice(firstModuleIndex, lastModuleIndex) + .map(m => ({ + name: m, + checked: ctx.session.selectedModules.includes(m) + })) + + modulesOnPage.forEach(m => { + message += `${m.checked ?'βœ…': '🟑'}/${m.name}\n`; + }) + + return message; + +} + +function createMenuRows(ctx){ + + const backBtn = ctx.session.page === 0 ? + Markup.button.callback(" ", "nothing") : + Markup.button.callback("⬅️", "page_back"); + + const lastPageIndex = Math.ceil(ctx.session.modules.length / MODULES_PER_PAGE) - 1; + + const forwardButton = ctx.session.page === lastPageIndex ? + Markup.button.callback(" ", "nothing") : + Markup.button.callback("➑️", "page_next") + + return [ + [ + Markup.button.callback("βœ… Select all", "select_all"), + Markup.button.callback("❌ Select none", "select_none"), + ], + [ + backBtn, + Markup.button.callback("βœ”οΈ Save", "save"), + forwardButton + ] + ]; +} + +function refreshMenu(ctx){ + try{ + ctx.editMessageText(createModulesList(ctx), { + message_id: ctx.session.menuMessageId, + reply_markup: { + inline_keyboard: createMenuRows(ctx) } }); - - keyboardRows.push([Markup.button.callback("πŸ“© subscribe", "complete")]); - keyboardRows.push([Markup.button.callback("🚫 Cancel", "cancel")]); - - return keyboardRows; + }catch(err){ + console.log(err); + + } + } \ No newline at end of file From 879e41c9e4b78ccf486b1e9bfdacd0adf411fb24 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 31 Mar 2026 12:27:25 +0300 Subject: [PATCH 12/57] feat: creating DB at Wisetrout DO bot module installation --- .../wisetrout_do_bot/wisetrout_do_bot.install | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install diff --git a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install new file mode 100644 index 0000000..f83eddf --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install @@ -0,0 +1,40 @@ + 'Stores data about subscriptions to the Telegram bot.', + 'fields' => [ + 'id' => [ + 'description' => 'Primary Key: Unique identifier for each entry.', + 'type' => 'serial', + 'not null' => TRUE, + ], + 'chat_id' => [ + 'description' => 'Chat id to be used for sending messages.', + 'type' => 'char', + 'length' => '10', + 'not null' => TRUE, + ], + 'module' => [ + 'description' => 'Id of the module user has subscribed to.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE + ], + 'created' => [ + 'description' => 'The time that the subscription was created.', + 'type' => 'datetime:normal', + 'not null' => TRUE, + 'mysql_type' => 'DATETIME', + ], + ], + 'primary key' => ['id'], + ]; + + return $schema; +} + From ae6ca68a992507f5de685ae9ff6a13de7c9920e6 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 31 Mar 2026 12:28:06 +0300 Subject: [PATCH 13/57] feat(bot): real requests to server for user info and subscription --- wisetrout-do-bot/api-calls/subscription.js | 71 +++++++++++++++++++- wisetrout-do-bot/bot.js | 5 +- wisetrout-do-bot/scenes/subscription-menu.js | 9 +-- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 2f3fd82..d7138df 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -1,10 +1,75 @@ -export async function subscribe(chatId, modules){} +import { oAuthToken } from "../oauth.js"; + +export async function subscribe(userInfo, modules){ + console.log('Creating subscription...'); + console.log(userInfo); + console.log('Modules:'); + console.log(modules); + + const body = modules ? + {userInfo, modules} : + {userInfo} + + try{ + const url = process.env.BASE_URL + '/api/telegram/subscribe'; + console.log(url); + + const res = await fetch(process.env.BASE_URL + '/api/telegram/subscribe', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${oAuthToken}` + }, + body + }) + + if(!res.ok) throw new Error(res.status); + + console.log('Success!'); + console.log(res); + + + const json = await res.json(); + + console.log('Data:'); + console.log(json); + + + + }catch(err){ + console.log(err); + return null; + } +} export async function unsubscribe(chatId){} export async function checkSubscription(chatId){ - await wait(2000); - return null; + try{ + + const res = await fetch(`${process.env.BASE_URL}/api/telegram/user-info/${chatId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${oAuthToken}` + } + }) + + if(!res.ok) throw new Error(res.status); + + console.log('user Info success!'); + console.log(res); + + + const json = await res.json(); + + console.log('Data:'); + console.log(json); + + + + }catch(err){ + console.log(err); + return null; + } } export async function wipeoutData(chatId){} diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 4093ce8..fbeb569 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -5,7 +5,7 @@ import { createActionsKeyboard } from './markup/keyboards.js'; import { scene as wipeoutScene } from './scenes/wipeout.js'; import { subscribe, unsubscribe } from './api-calls/subscription.js'; import { Stage } from 'telegraf/scenes'; -import { activateOAuthToken, oAuthToken } from './oauth.js'; +import { activateOAuthToken } from './oauth.js'; activateOAuthToken() .then(() => { @@ -27,9 +27,6 @@ function activateBot(){ bot.start(ctx => { - ctx.reply(oAuthToken ? 'Token created' : 'Could not create token') - - ctx.reply( 'Welcome to the Wisetrout Drupal org bot!', createActionsKeyboard(ctx) diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 04ece04..80ff3eb 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -22,7 +22,7 @@ scene.enter(async ctx => { scene.action("all", async ctx => { await Promise.all([ ctx.reply('Subscribing to updates...'), - subscribe(ctx.from.id) + subscribe(ctx.from) ]); ctx.session.userInfo = { @@ -76,11 +76,8 @@ scene.action("page_next", ctx => { scene.command(/.+/, async ctx => { const { command } = ctx; - console.log('Command:'); - console.log(command); - - if(['page_back', 'page_next', 'complete', 'cancel', 'all', 'specific', ''].includes(command)) return; + if(['page_back', 'page_next', 'save', 'cancel', 'all', 'specific', ''].includes(command)) return; if(ctx.session.selectedModules.includes(command)){ @@ -113,7 +110,7 @@ scene.action("save", async ctx => { ctx.session.selectedModules; await Promise.all([ ctx.reply('Subscribing to updates..'), - subscribe(ctx.from.id, modulesToSubscribe) + subscribe(ctx.from, modulesToSubscribe) ]); ctx.session.userInfo = { subscribed: true, From d959ac240f185ce120c571aaad8d5c8bc6a9eaaf Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 31 Mar 2026 12:31:46 +0300 Subject: [PATCH 14/57] config related to Telegram bot --- ...resource.wisetrout_do_bot_modules_list.yml | 18 +++++++++++++++ ...st.resource.wisetrout_do_bot_subscribe.yml | 18 +++++++++++++++ ....resource.wisetrout_do_bot_unsubscribe.yml | 18 +++++++++++++++ ...st.resource.wisetrout_do_bot_user_info.yml | 18 +++++++++++++++ ...rest.resource.wisetrout_do_bot_wipeout.yml | 18 +++++++++++++++ .../sync/simple_oauth.oauth2_scope.tgbot.yml | 22 +++++++++++++++++++ config/sync/simple_oauth.settings.yml | 2 ++ config/sync/user.role.telegram_bot.yml | 17 ++++++++++++-- 8 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 config/sync/rest.resource.wisetrout_do_bot_modules_list.yml create mode 100644 config/sync/rest.resource.wisetrout_do_bot_subscribe.yml create mode 100644 config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml create mode 100644 config/sync/rest.resource.wisetrout_do_bot_user_info.yml create mode 100644 config/sync/rest.resource.wisetrout_do_bot_wipeout.yml create mode 100644 config/sync/simple_oauth.oauth2_scope.tgbot.yml diff --git a/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml b/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml new file mode 100644 index 0000000..1235679 --- /dev/null +++ b/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml @@ -0,0 +1,18 @@ +uuid: 058ac28b-3692-4441-bc1c-1f55faa85dfd +langcode: en +status: true +dependencies: + module: + - serialization + - simple_oauth + - wisetrout_do_bot +id: wisetrout_do_bot_modules_list +plugin_id: wisetrout_do_bot_modules_list +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - oauth2 diff --git a/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml b/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml new file mode 100644 index 0000000..88da362 --- /dev/null +++ b/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml @@ -0,0 +1,18 @@ +uuid: 183a38d1-bc4f-42bc-a5ea-a4882959bf60 +langcode: en +status: true +dependencies: + module: + - serialization + - simple_oauth + - wisetrout_do_bot +id: wisetrout_do_bot_subscribe +plugin_id: wisetrout_do_bot_subscribe +granularity: resource +configuration: + methods: + - POST + formats: + - json + authentication: + - oauth2 diff --git a/config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml b/config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml new file mode 100644 index 0000000..5f77a46 --- /dev/null +++ b/config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml @@ -0,0 +1,18 @@ +uuid: 2b20c227-5dd8-4901-bb5e-9f1fb3979f31 +langcode: en +status: true +dependencies: + module: + - serialization + - simple_oauth + - wisetrout_do_bot +id: wisetrout_do_bot_unsubscribe +plugin_id: wisetrout_do_bot_unsubscribe +granularity: resource +configuration: + methods: + - POST + formats: + - json + authentication: + - oauth2 diff --git a/config/sync/rest.resource.wisetrout_do_bot_user_info.yml b/config/sync/rest.resource.wisetrout_do_bot_user_info.yml new file mode 100644 index 0000000..0b2ea52 --- /dev/null +++ b/config/sync/rest.resource.wisetrout_do_bot_user_info.yml @@ -0,0 +1,18 @@ +uuid: c7bfac03-2011-4acf-a043-af30ac97cece +langcode: en +status: true +dependencies: + module: + - serialization + - simple_oauth + - wisetrout_do_bot +id: wisetrout_do_bot_user_info +plugin_id: wisetrout_do_bot_user_info +granularity: resource +configuration: + methods: + - GET + formats: + - json + authentication: + - oauth2 diff --git a/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml b/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml new file mode 100644 index 0000000..895c78a --- /dev/null +++ b/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml @@ -0,0 +1,18 @@ +uuid: 6eb95e26-faa7-49a0-9f18-bed9b7f9ad14 +langcode: en +status: true +dependencies: + module: + - serialization + - simple_oauth + - wisetrout_do_bot +id: wisetrout_do_bot_wipeout +plugin_id: wisetrout_do_bot_wipeout +granularity: resource +configuration: + methods: + - POST + formats: + - json + authentication: + - oauth2 diff --git a/config/sync/simple_oauth.oauth2_scope.tgbot.yml b/config/sync/simple_oauth.oauth2_scope.tgbot.yml new file mode 100644 index 0000000..2e02698 --- /dev/null +++ b/config/sync/simple_oauth.oauth2_scope.tgbot.yml @@ -0,0 +1,22 @@ +uuid: 4284b1cc-eac2-403f-9b94-db47b6132ebe +langcode: en +status: true +dependencies: { } +id: tgbot +name: tgbot +description: tgbot +grant_types: + refresh_token: + status: true + description: '' + client_credentials: + status: true + description: '' + authorization_code: + status: true + description: '' +umbrella: false +parent: _none +granularity_id: role +granularity_configuration: + role: telegram_bot diff --git a/config/sync/simple_oauth.settings.yml b/config/sync/simple_oauth.settings.yml index 06b4a50..521a856 100644 --- a/config/sync/simple_oauth.settings.yml +++ b/config/sync/simple_oauth.settings.yml @@ -2,4 +2,6 @@ _core: default_config_hash: ci56aVE-Jo4w_Xy5Gkk1coAwO-fj7Q87l0PKc5E9sfo scope_provider: dynamic token_cron_batch_size: 0 +public_key: ../private/simple_oauth/public.key +private_key: ../private/simple_oauth/private.key disable_openid_connect: false diff --git a/config/sync/user.role.telegram_bot.yml b/config/sync/user.role.telegram_bot.yml index 48bcdba..51daeef 100644 --- a/config/sync/user.role.telegram_bot.yml +++ b/config/sync/user.role.telegram_bot.yml @@ -1,9 +1,22 @@ uuid: ee20a0d5-2658-4a72-bfb5-4d6931951d7a langcode: en status: true -dependencies: { } +dependencies: + config: + - rest.resource.wisetrout_do_bot_modules_list + - rest.resource.wisetrout_do_bot_subscribe + - rest.resource.wisetrout_do_bot_unsubscribe + - rest.resource.wisetrout_do_bot_user_info + - rest.resource.wisetrout_do_bot_wipeout + module: + - rest id: telegram_bot label: 'Telegram bot' weight: 4 is_admin: false -permissions: { } +permissions: + - 'restful get wisetrout_do_bot_modules_list' + - 'restful get wisetrout_do_bot_user_info' + - 'restful post wisetrout_do_bot_subscribe' + - 'restful post wisetrout_do_bot_unsubscribe' + - 'restful post wisetrout_do_bot_wipeout' From 8fab24bb2c07abeadb7dabafd0dedd7c5859e6e0 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 2 Apr 2026 12:01:55 +0300 Subject: [PATCH 15/57] feat: subscribing to tg bot and reading existing subscriptions --- .../src/Plugin/rest/resource/Subscribe.php | 61 ++++++++++++++++++- .../src/Plugin/rest/resource/UserInfo.php | 32 +++++++++- .../wisetrout_do_bot/wisetrout_do_bot.install | 13 +++- wisetrout-do-bot/api-calls/subscription.js | 23 ++++--- 4 files changed, 113 insertions(+), 16 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 4fab0f5..ac85063 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -4,6 +4,7 @@ use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a resource to subscribe to Telegram bot. @@ -12,13 +13,67 @@ * id = "wisetrout_do_bot_subscribe", * label = @Translation("Telegram bot subscription"), * uri_paths = { - * "canonical" = "/api/telegram/subscribe" + * "create" = "/api/telegram/subscribe" * } * ) */ class Subscribe extends ResourceBase { + +protected $currentRequest; + +public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); + return $instance; + } + + public function post() { - $data = ['userInfo' => NULL]; - return new ResourceResponse($data); + $content = $this->currentRequest->getContent(); + $params = json_decode($content, TRUE); + + $cid = strval($params['userInfo']['id']); + + $database = \Drupal::database(); + $selectQuery = $database->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); + $existingSubscriptions = $selectQuery->fetchAll(); + + $insertQuery = $database->insert('telegram_subscriptions')->fields(['chat_id', 'module', 'created', 'status']); + + if($params['modules']){ + + foreach($params['modules'] as $module) { + $existingSubscription = array_find($existingSubscriptions, function($value){ + return $value['module'] === $module; + }); + + if(!$existingSubscription) { + $valuesToInsert = [ + 'chat_id' => $cid, + 'module' => $module, + 'created' => $params['timestamp'], + 'status' => 1 + ]; + $insertQuery->values($valuesToInsert); + } + } + + } + + // } else { + // // If modules === null, this means user has subscribed to all updates + // } + + $insertQuery->execute(); + + // $testQuery = $database->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); + // $testResult = $testQuery->fetchAll(); + + // $testJson = json_encode($testResult); + + return new ResourceResponse([ + 'message' => 'success', + 'cid' => $cid + ]); } } \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index e54dde6..1dad577 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -17,8 +17,34 @@ * ) */ class UserInfo extends ResourceBase { - public function get($cid = NULL) { - $data = ['userInfo' => NULL]; - return new ResourceResponse($data); + + public function get($cid){ + + $database = \Drupal::database(); + $selectQuery = $database->query("SELECT module, status FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); + $result = $selectQuery->fetchAll(); + + $modules = []; + + if(count($result) === 0) { + $data = NULL; + } else { + + $modules = []; + + foreach($result as $resultRow){ + $modules[] = $resultRow->module; + } + + $data = [ + 'modules' => $modules, + 'subscribed' => $result[0]->status == 1, + ]; + } + + + return new ResourceResponse([ + 'userInfo' => $data, + ]); } } \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install index f83eddf..4a74c90 100644 --- a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install +++ b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install @@ -27,10 +27,19 @@ function wisetrout_do_bot_schema() { ], 'created' => [ 'description' => 'The time that the subscription was created.', - 'type' => 'datetime:normal', + 'type' => 'int', + 'size' => 'big', 'not null' => TRUE, - 'mysql_type' => 'DATETIME', + 'unsigned' => TRUE, ], + 'status' => [ + 'description' => 'Whether or not the subscription is active. 1 = active, 0 = inactive.', + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + ] ], 'primary key' => ['id'], ]; diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index d7138df..2a45f86 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -5,19 +5,19 @@ export async function subscribe(userInfo, modules){ console.log(userInfo); console.log('Modules:'); console.log(modules); + const timestamp = Date.now(); - const body = modules ? - {userInfo, modules} : - {userInfo} + const body = JSON.stringify(modules ? + {userInfo, modules, timestamp} : + {userInfo, timestamp}) try{ - const url = process.env.BASE_URL + '/api/telegram/subscribe'; - console.log(url); const res = await fetch(process.env.BASE_URL + '/api/telegram/subscribe', { method: 'POST', headers: { - 'Authorization': `Bearer ${oAuthToken}` + 'Authorization': `Bearer ${oAuthToken}`, + 'Content-Type': 'application/json' }, body }) @@ -59,10 +59,17 @@ export async function checkSubscription(chatId){ console.log(res); - const json = await res.json(); + const data = await res.json(); console.log('Data:'); - console.log(json); + console.log(data); + + + console.log('User info:'); + console.log(data.userInfo); + + + return data.userInfo; From 0d1e8f307a29f39c0bd268d8bdb399bf95ae618d Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 2 Apr 2026 12:13:49 +0300 Subject: [PATCH 16/57] removed "subscribe to all modules" button in chat bot --- wisetrout-do-bot/scenes/subscription-menu.js | 39 +------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 80ff3eb..55791ba 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -7,33 +7,6 @@ const MODULES_PER_PAGE = 10; export const scene = new Scenes.BaseScene('subscription-menu'); scene.enter(async ctx => { - ctx.reply("Subscribe to:", - Markup.inlineKeyboard([ - [ - Markup.button.callback("βœ… All updates", "all"), - Markup.button.callback("βš™οΈ Specific modules", "specific") - ], - [ - Markup.button.callback("🚫 Cancel", "cancel") - ] - ])) -}); - -scene.action("all", async ctx => { - await Promise.all([ - ctx.reply('Subscribing to updates...'), - subscribe(ctx.from) - ]); - - ctx.session.userInfo = { - subscribed: true, - modules: null - } - ctx.reply('Subscription successful!'); - ctx.scene.leave(); -}) - -scene.action("specific", async ctx => { try{ const [modules] = await Promise.all([ @@ -77,7 +50,7 @@ scene.action("page_next", ctx => { scene.command(/.+/, async ctx => { const { command } = ctx; - if(['page_back', 'page_next', 'save', 'cancel', 'all', 'specific', ''].includes(command)) return; + if(['page_back', 'page_next', 'save', 'nothing', 'select_all', 'select_none'].includes(command)) return; if(ctx.session.selectedModules.includes(command)){ @@ -123,16 +96,6 @@ scene.action("save", async ctx => { ctx.scene.leave(); }) - - -scene.action("cancel", ctx => { - ctx.reply("Subscription process cancelled"); - if(ctx.session.selectedModules) delete ctx.session.selectedModules; - if(ctx.session.modules) delete ctx.session.modules; - delete ctx.session.menuMessageId; - ctx.scene.leave(); -}); - function createModulesList(ctx){ let message = ''; const firstModuleIndex = ctx.session.page * MODULES_PER_PAGE; From 8eb2f7a154e215535d099b1ea86bee4a80e4a5a0 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 2 Apr 2026 12:16:25 +0300 Subject: [PATCH 17/57] removed "all modules" option from Telegram subscription endpoint --- .../src/Plugin/rest/resource/Subscribe.php | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index ac85063..624e28b 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -40,37 +40,25 @@ public function post() { $insertQuery = $database->insert('telegram_subscriptions')->fields(['chat_id', 'module', 'created', 'status']); - if($params['modules']){ - - foreach($params['modules'] as $module) { - $existingSubscription = array_find($existingSubscriptions, function($value){ - return $value['module'] === $module; - }); - - if(!$existingSubscription) { - $valuesToInsert = [ - 'chat_id' => $cid, - 'module' => $module, - 'created' => $params['timestamp'], - 'status' => 1 - ]; - $insertQuery->values($valuesToInsert); - } + foreach($params['modules'] as $module) { + $existingSubscription = array_find($existingSubscriptions, function($value){ + return $value['module'] === $module; + }); + + if(!$existingSubscription) { + $valuesToInsert = [ + 'chat_id' => $cid, + 'module' => $module, + 'created' => $params['timestamp'], + 'status' => 1 + ]; + $insertQuery->values($valuesToInsert); } - } - // } else { - // // If modules === null, this means user has subscribed to all updates - // } $insertQuery->execute(); - // $testQuery = $database->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); - // $testResult = $testQuery->fetchAll(); - - // $testJson = json_encode($testResult); - return new ResourceResponse([ 'message' => 'success', 'cid' => $cid From d545edbb477a5a5e508d71ef73d827f3222fad72 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 2 Apr 2026 13:18:21 +0300 Subject: [PATCH 18/57] modules list endpoint - simplified response --- .../src/Plugin/rest/resource/ModulesList.php | 8 +++++++- wisetrout-do-bot/api-calls/modules-list.js | 11 +---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php index 8e02a72..3e7a54e 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php @@ -27,6 +27,12 @@ public function get() { $nodes = \Drupal\node\Entity\node::loadMultiple($nids); - return new ResourceResponse($nodes); + $nodeNames = []; + + foreach($nodes as $node){ + $nodeNames[] = $node->field_module_machine_name[0]->value; + } + + return new ResourceResponse($nodeNames); } } \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/modules-list.js b/wisetrout-do-bot/api-calls/modules-list.js index 060bd2f..ee26842 100644 --- a/wisetrout-do-bot/api-calls/modules-list.js +++ b/wisetrout-do-bot/api-calls/modules-list.js @@ -10,19 +10,10 @@ export async function getModulesList(){ if(!response.ok) throw new Error(response.status); - const resObj = await response.json(); - - const modules = []; - for(const id in resObj){ - - const { field_module_machine_name } = resObj[id]; - - modules.push(field_module_machine_name[0].value); - } + const modules = await response.json(); modules.sort(); - return modules; } From 9fe062e1fb17bbf208ea4cafb612c2597ee40b03 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 7 Apr 2026 13:56:31 +0300 Subject: [PATCH 19/57] changed to Telegram bot DB schema --- .../wisetrout_do_bot/wisetrout_do_bot.install | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install index 4a74c90..d3de12d 100644 --- a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install +++ b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install @@ -19,11 +19,11 @@ function wisetrout_do_bot_schema() { 'length' => '10', 'not null' => TRUE, ], - 'module' => [ + 'module_id' => [ 'description' => 'Id of the module user has subscribed to.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE + 'type' => 'int', + 'not null' => TRUE, + 'unsigned' => TRUE ], 'created' => [ 'description' => 'The time that the subscription was created.', @@ -31,17 +31,35 @@ function wisetrout_do_bot_schema() { 'size' => 'big', 'not null' => TRUE, 'unsigned' => TRUE, + ] + ], + 'primary key' => ['id'], + ]; + + $schema['telegram_subscribers'] = [ + 'description' => 'Stores data about Telegram users subscribed to the bot.', + 'fields' => [ + 'chat_id' => [ + 'description' => 'Chat id to be used for sending messages.', + 'type' => 'char', + 'length' => '10', + 'not null' => TRUE, ], 'status' => [ - 'description' => 'Whether or not the subscription is active. 1 = active, 0 = inactive.', + 'description' => 'Status of subscription. 1 = active, 0 = inactive.', 'type' => 'int', 'size' => 'tiny', - 'unsigned' => TRUE, + 'not null' => TRUE + ], + 'created' => [ + 'description' => 'The time that the entry was created.', + 'type' => 'int', + 'size' => 'big', 'not null' => TRUE, - 'default' => 1, - ] + 'unsigned' => TRUE, + ] ], - 'primary key' => ['id'], + 'primary key' => ['chat_id'], ]; return $schema; From 4cc62500102404131b7082f2076047d79aaf6860 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 7 Apr 2026 14:05:18 +0300 Subject: [PATCH 20/57] started working on subscription logic --- .../src/Plugin/rest/resource/Subscribe.php | 76 +++++++++++++++---- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 624e28b..3952814 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -30,38 +30,84 @@ public static function create(ContainerInterface $container, array $configuratio public function post() { $content = $this->currentRequest->getContent(); + $params = json_decode($content, TRUE); + + $responseData = []; $cid = strval($params['userInfo']['id']); $database = \Drupal::database(); - $selectQuery = $database->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); - $existingSubscriptions = $selectQuery->fetchAll(); - $insertQuery = $database->insert('telegram_subscriptions')->fields(['chat_id', 'module', 'created', 'status']); + $subscriberData = $database->query("SELECT * FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid])->fetchField(); - foreach($params['modules'] as $module) { - $existingSubscription = array_find($existingSubscriptions, function($value){ - return $value['module'] === $module; + $responseData['user exists'] = !!$subscriberData; + + if(!$subscriberData){ + $subsriberCreationResult = $database->insert('telegram_subscribers') + ->fields([ + 'chat_id' => $cid, + 'status' => 1, + 'created' => $params['timestamp'] + ]) + ->execute(); + } + + $existingSubscriptions = $database + ->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) + ->fetchAll(); + + $moduleIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_module') + ->accessCheck(FALSE) + ->condition('field_module_machine_name', $params['modules'], 'IN') + ->execute(); + + + // Subscribe to new modules + $modulesIdsToAdd = []; + $insertQuery = $database + ->insert('telegram_subscriptions') + ->fields(['chat_id', 'module_id', 'created']); + + + foreach($moduleIds as $moduleId){ + + $subscriptionExists = array_find($existingSubscriptions, function($existingSubscription){ + return $existingSubscription->module_id === $moduleId; }); - if(!$existingSubscription) { + if(!$subscriptionExists){ + $moduleIdsToAdd[] = $moduleId; $valuesToInsert = [ 'chat_id' => $cid, - 'module' => $module, - 'created' => $params['timestamp'], - 'status' => 1 + 'module_id' => $moduleId, + 'created' => $params['timestamp'] ]; $insertQuery->values($valuesToInsert); } } + // $insertQuery->execute(); + + // Unsubscribe from modules + + $moduleIdsToDelete = []; + + foreach($existingSubscriptions as $existingSubscription){ + $keepSubscription = array_find($moduleIds, function($moduleId){ + return $moduleId === $existingSubscription->module_id; + }); + if(!$keepSubscription){ + $moduleIdsToDelete[] = $existingSubscription->module_id; + } + } + + $responseData['to delete'] = $moduleIdsToDelete; + $responseData['to add'] = $moduleIdsToAdd; - $insertQuery->execute(); + - return new ResourceResponse([ - 'message' => 'success', - 'cid' => $cid - ]); + return new ResourceResponse($responseData); } } \ No newline at end of file From ab5c022b29de9fd57e60ed3a8b9d3a7434ef5231 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 8 Apr 2026 12:24:28 +0300 Subject: [PATCH 21/57] working on userInfo endpoint --- .../src/Plugin/rest/resource/UserInfo.php | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 1dad577..8b0d67a 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -4,6 +4,7 @@ use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Drupal\node\Entity\Node; /** * Provides a resource to get Telegram bot user info. @@ -18,33 +19,56 @@ */ class UserInfo extends ResourceBase { - public function get($cid){ + private function readUserStatus($cid, $database){ + $queryResult = $database + ->query("SELECT status FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid]) + ->fetchAssoc(); + $status = $queryResult ? $queryResult['status'] : null; + return $status; + } - $database = \Drupal::database(); - $selectQuery = $database->query("SELECT module, status FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]); - $result = $selectQuery->fetchAll(); + private function readModulesList($cid, $database){ - $modules = []; + $dbRows = $database + ->query("SELECT module_id FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) + ->fetchAll(); - if(count($result) === 0) { - $data = NULL; - } else { + $moduleIds = []; + + foreach($dbRows as $dbRow){ + $moduleIds[] = $dbRow->module_id; + } + + $nodes = \Drupal\node\Entity\node::loadMultiple($nids); $modules = []; - foreach($result as $resultRow){ - $modules[] = $resultRow->module; + foreach($nodes as $node){ + $modules[] = $node->field_module_machine_name[0]->value; } - $data = [ - 'modules' => $modules, - 'subscribed' => $result[0]->status == 1, - ]; + return $modules; + + // return $moduleIds; } + public function get($cid){ + + $database = \Drupal::database(); + + $userStatus = $this->readUserStatus($cid, $database); + $responseData; + + if($userStatus === null) { + $responseData = null; + }else { + $modulesList = $this->readModulesList($cid, $database); + $responseData = [ + 'subscribed' => !!$userStatus, + 'modules' => $modulesList, + ]; + } - return new ResourceResponse([ - 'userInfo' => $data, - ]); + return new ResourceResponse($responseData); } } \ No newline at end of file From 3d08c4af3a7cc2576871c4010ca53ae6546216d5 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 8 Apr 2026 12:43:31 +0300 Subject: [PATCH 22/57] fix: Telegram user info endpoint --- .../wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 8b0d67a..737d95f 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -39,7 +39,7 @@ private function readModulesList($cid, $database){ $moduleIds[] = $dbRow->module_id; } - $nodes = \Drupal\node\Entity\node::loadMultiple($nids); + $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($moduleIds); $modules = []; From e4b376b4d1c39afb2893b48570f7abe357376a08 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 8 Apr 2026 12:48:56 +0300 Subject: [PATCH 23/57] fix: modules list endpoint --- .../wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php index 3e7a54e..397ef9b 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/ModulesList.php @@ -5,6 +5,7 @@ use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Drupal\node\Entity\Node; /** * Provides a resource to get list of modules user can subscribe to. @@ -25,7 +26,7 @@ public function get() { ->accessCheck(FALSE) ->execute(); - $nodes = \Drupal\node\Entity\node::loadMultiple($nids); + $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); $nodeNames = []; From e0eb80ccf50ca65b100d2515522fb23988badabe Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 8 Apr 2026 16:00:36 +0300 Subject: [PATCH 24/57] Telegram subscription endpoint - full functionality --- .../src/Plugin/rest/resource/Subscribe.php | 87 +++++++++++-------- wisetrout-do-bot/api-calls/subscription.js | 17 ++-- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 3952814..86df837 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -19,54 +19,48 @@ */ class Subscribe extends ResourceBase { -protected $currentRequest; + protected $currentRequest; + private $database; -public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); - $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); - return $instance; - } - - - public function post() { - $content = $this->currentRequest->getContent(); - - $params = json_decode($content, TRUE); - - $responseData = []; + private function createUserIfNonExistent($cid){ - $cid = strval($params['userInfo']['id']); - - $database = \Drupal::database(); - - $subscriberData = $database->query("SELECT * FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid])->fetchField(); - - $responseData['user exists'] = !!$subscriberData; + $subscriberData = $this->database + ->query("SELECT * FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid]) + ->fetchField(); if(!$subscriberData){ - $subsriberCreationResult = $database->insert('telegram_subscribers') + $subsriberCreationResult = $this->database->insert('telegram_subscribers') ->fields([ 'chat_id' => $cid, 'status' => 1, - 'created' => $params['timestamp'] + 'created' => $this->currentRequest->server->get('REQUEST_TIME'), ]) ->execute(); + return TRUE; } - $existingSubscriptions = $database + return FALSE; + } + + private function updateModulesList($cid, $moduleNames){ + + $existingSubscriptions = $this->database ->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) ->fetchAll(); $moduleIds = \Drupal::entityQuery('node') ->condition('type', 'ai_module') ->accessCheck(FALSE) - ->condition('field_module_machine_name', $params['modules'], 'IN') + ->condition('field_module_machine_name', $moduleNames, 'IN') ->execute(); - - // Subscribe to new modules - $modulesIdsToAdd = []; - $insertQuery = $database + $this->subscribeToNewModules($cid, $moduleIds, $existingSubscriptions); + $this->unsubscribeFromOldModules($cid, $moduleIds, $existingSubscriptions); + } + + private function subscribeToNewModules($cid, $moduleIds, $existingSubscriptions){ + + $insertQuery = $this->database ->insert('telegram_subscriptions') ->fields(['chat_id', 'module_id', 'created']); @@ -78,19 +72,19 @@ public function post() { }); if(!$subscriptionExists){ - $moduleIdsToAdd[] = $moduleId; $valuesToInsert = [ 'chat_id' => $cid, 'module_id' => $moduleId, - 'created' => $params['timestamp'] + 'created' => $this->currentRequest->server->get('REQUEST_TIME'), ]; $insertQuery->values($valuesToInsert); } } - // $insertQuery->execute(); + $insertQuery->execute(); + } - // Unsubscribe from modules + private function unsubscribeFromOldModules($cid, $moduleIds, $existingSubscriptions){ $moduleIdsToDelete = []; @@ -103,11 +97,32 @@ public function post() { } } - $responseData['to delete'] = $moduleIdsToDelete; - $responseData['to add'] = $moduleIdsToAdd; + $this->database + ->delete('telegram_subscriptions') + ->condition('module_id', $moduleIdsToDelete, 'IN') + ->execute(); + + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); + $instance->database = \Drupal::database(); + return $instance; + } + + public function post() { + $content = $this->currentRequest->getContent(); + $params = json_decode($content, TRUE); + + $cid = strval($params['userInfo']['id']); + $moduleNames = $params['modules']; + + $userCreated = $this->createUserIfNonExistent($cid); + $this->updateModulesList($cid, $moduleNames); - return new ResourceResponse($responseData); + return new ResourceResponse(['userCreated' => $userCreated]); } } \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 2a45f86..b530be7 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -5,11 +5,14 @@ export async function subscribe(userInfo, modules){ console.log(userInfo); console.log('Modules:'); console.log(modules); - const timestamp = Date.now(); const body = JSON.stringify(modules ? - {userInfo, modules, timestamp} : - {userInfo, timestamp}) + {userInfo, modules} : + {userInfo}); + + console.log('Body:'); + console.log(body); + try{ @@ -61,15 +64,11 @@ export async function checkSubscription(chatId){ const data = await res.json(); - console.log('Data:'); - console.log(data); - - console.log('User info:'); - console.log(data.userInfo); + console.log(data); - return data.userInfo; + return data; From eb365182ea8d72af3fc70177f597b5031d2061f5 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 10:19:43 +0300 Subject: [PATCH 25/57] feat(Telegram bot): unsubscribing (pausing subscription) --- .../src/Plugin/rest/resource/Unsubscribe.php | 32 ++++++++++++++-- wisetrout-do-bot/api-calls/subscription.js | 38 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php index 8d97126..499f879 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php @@ -4,6 +4,7 @@ use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a resource to unsubscribe from Telegram bot. @@ -12,13 +13,38 @@ * id = "wisetrout_do_bot_unsubscribe", * label = @Translation("Telegram bot unsubscription"), * uri_paths = { - * "canonical" = "/api/telegram/unsubscribe" + * "create" = "/api/telegram/unsubscribe" * } * ) */ class Unsubscribe extends ResourceBase { + + protected $currentRequest; + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); + return $instance; + } + + public function post() { - $data = ['userInfo' => NULL]; - return new ResourceResponse($data); + $content = $this->currentRequest->getContent(); + + $params = json_decode($content, TRUE); + + $cid = strval($params['chatId']); + + $database = \Drupal::database(); + + $database + ->update('telegram_subscribers') + ->fields([ + 'status' => 0, + ]) + ->condition('chat_id', $cid) + ->execute(); + + return new ResourceResponse(['message' => 'success!']); } } \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index b530be7..6d8697b 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -44,7 +44,43 @@ export async function subscribe(userInfo, modules){ } } -export async function unsubscribe(chatId){} +export async function unsubscribe(chatId){ + console.log('Unsubscribing...'); + + const body = JSON.stringify({ chatId }); + + console.log('Body:'); + console.log(body); + + + try{ + + const res = await fetch(process.env.BASE_URL + '/api/telegram/unsubscribe', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${oAuthToken}`, + 'Content-Type': 'application/json' + }, + body + }) + + if(!res.ok) throw new Error(res.status); + + console.log('Success!'); + console.log(res); + + + const json = await res.json(); + + console.log('Data:'); + console.log(json); + + + + }catch(err){ + console.log(err); + } +} export async function checkSubscription(chatId){ try{ From 91283d51a8d75c0265425b4c0dcce71c0a058e1d Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 11:13:29 +0300 Subject: [PATCH 26/57] feat(Telegram bot): reactivating subscription --- .../resource/{Unsubscribe.php => UpdateStatus.php} | 11 ++++++----- wisetrout-do-bot/api-calls/subscription.js | 8 ++++---- wisetrout-do-bot/bot.js | 6 +++--- 3 files changed, 13 insertions(+), 12 deletions(-) rename web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/{Unsubscribe.php => UpdateStatus.php} (77%) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php similarity index 77% rename from web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php rename to web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php index 499f879..f8075e8 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Unsubscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php @@ -7,17 +7,17 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Provides a resource to unsubscribe from Telegram bot. + * Provides a resource to pause/renew subscription to Telegram bot. * * @RestResource( * id = "wisetrout_do_bot_unsubscribe", - * label = @Translation("Telegram bot unsubscription"), + * label = @Translation("Telegram bot pause/renew subscription"), * uri_paths = { - * "create" = "/api/telegram/unsubscribe" + * "create" = "/api/telegram/update-status" * } * ) */ -class Unsubscribe extends ResourceBase { +class UpdateStatus extends ResourceBase { protected $currentRequest; @@ -34,13 +34,14 @@ public function post() { $params = json_decode($content, TRUE); $cid = strval($params['chatId']); + $newStatus = $params['subscribed'] ? 1 : 0; $database = \Drupal::database(); $database ->update('telegram_subscribers') ->fields([ - 'status' => 0, + 'status' => $newStatus, ]) ->condition('chat_id', $cid) ->execute(); diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 6d8697b..586830e 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -44,10 +44,10 @@ export async function subscribe(userInfo, modules){ } } -export async function unsubscribe(chatId){ - console.log('Unsubscribing...'); +export async function updateStatus(chatId, subscribed){ + console.log('Updating status...'); - const body = JSON.stringify({ chatId }); + const body = JSON.stringify({ chatId, subscribed }); console.log('Body:'); console.log(body); @@ -55,7 +55,7 @@ export async function unsubscribe(chatId){ try{ - const res = await fetch(process.env.BASE_URL + '/api/telegram/unsubscribe', { + const res = await fetch(process.env.BASE_URL + '/api/telegram/update-status', { method: 'POST', headers: { 'Authorization': `Bearer ${oAuthToken}`, diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index fbeb569..948fc42 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -3,7 +3,7 @@ import preferencesMiddleware from './middlewares/preferences-middleware.js'; import { scene as subscrScene } from './scenes/subscription-menu.js'; import { createActionsKeyboard } from './markup/keyboards.js'; import { scene as wipeoutScene } from './scenes/wipeout.js'; -import { subscribe, unsubscribe } from './api-calls/subscription.js'; +import { updateStatus } from './api-calls/subscription.js'; import { Stage } from 'telegraf/scenes'; import { activateOAuthToken } from './oauth.js'; @@ -40,7 +40,7 @@ function activateBot(){ bot.action('unsub', async ctx => { await Promise.all([ ctx.reply('Cancelling your subscription...'), - unsubscribe(ctx.from.id) + updateStatus(ctx.from.id, false) ]); ctx.session.userInfo.subscribed = false; @@ -53,7 +53,7 @@ function activateBot(){ await Promise.all([ ctx.reply('Reactivating your subscription...'), - subscribe(ctx.from.id, ctx.session.userInfo.modules) + updateStatus(ctx.from.id, true) ]); ctx.session.userInfo.subscribed = true; From cad355d99125464176a2d74f49e4120e3db1763b Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 12:13:02 +0300 Subject: [PATCH 27/57] feat(Telegram bot): user info wipeout --- .../src/Plugin/rest/resource/Wipeout.php | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php index ddaeb0c..7d3966e 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php @@ -4,6 +4,7 @@ use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a resource to wipeout all user data related to Telegram bot. @@ -12,13 +13,45 @@ * id = "wisetrout_do_bot_wipeout", * label = @Translation("Telegram bot wipeout"), * uri_paths = { - * "canonical" = "/api/telegram/wipeout" + * "create" = "/api/telegram/wipeout" * } * ) */ class Wipeout extends ResourceBase { + protected $currentRequest; + + private function deleteUserInfo($cid){ + $this->database + ->delete('telegram_subscribers') + ->condition('chat_id', $cid) + ->execute(); + } + + private function deleteUserSubscriptions($cid){ + $this->database + ->delete('telegram_subscriptions') + ->condition('chat_id', $cid) + ->execute(); + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); + $instance->database = \Drupal::database(); + return $instance; + } + + public function post() { - $data = ['userInfo' => NULL]; - return new ResourceResponse($data); + $content = $this->currentRequest->getContent(); + + $params = json_decode($content, TRUE); + + $cid = $params['chatId']; + + $this->deleteUserInfo($cid); + $this->deleteUserSubscriptions($cid); + + return new ResourceResponse(['message' => 'success!']); } } \ No newline at end of file From 3dfc3904ecd2d2598c4fb0f81d328988a72e3fc6 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 12:17:34 +0300 Subject: [PATCH 28/57] feat(Telegram bot): user wipeout - on the bot side --- wisetrout-do-bot/api-calls/subscription.js | 41 ++++++++++++++++++---- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 586830e..66bc3b9 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -114,11 +114,40 @@ export async function checkSubscription(chatId){ } } -export async function wipeoutData(chatId){} +export async function wipeoutData(chatId){ + console.log('Wipeout...'); -// Imitation of server request taking time -async function wait(ms){ - return await new Promise((res, rej) => { - setTimeout(() => res(), ms) - }) + const body = JSON.stringify({ chatId }); + + console.log('Body:'); + console.log(body); + + + try{ + + const res = await fetch(process.env.BASE_URL + '/api/telegram/wipeout', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${oAuthToken}`, + 'Content-Type': 'application/json' + }, + body + }) + + if(!res.ok) throw new Error(res.status); + + console.log('Success!'); + console.log(res); + + + const json = await res.json(); + + console.log('Data:'); + console.log(json); + + + + }catch(err){ + console.log(err); + } } \ No newline at end of file From c73221ccb6b8787bf3479f2459adc2142bf9b3a1 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 15:06:06 +0300 Subject: [PATCH 29/57] fix(Telegram bot): subscribe endpoint --- .../src/Plugin/rest/resource/Subscribe.php | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 86df837..13ea19e 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -7,7 +7,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Provides a resource to subscribe to Telegram bot. + * Provides a resource to update list of modules to follow via Telegram bot. * * @RestResource( * id = "wisetrout_do_bot_subscribe", @@ -44,18 +44,25 @@ private function createUserIfNonExistent($cid){ private function updateModulesList($cid, $moduleNames){ - $existingSubscriptions = $this->database - ->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) - ->fetchAll(); - - $moduleIds = \Drupal::entityQuery('node') - ->condition('type', 'ai_module') - ->accessCheck(FALSE) - ->condition('field_module_machine_name', $moduleNames, 'IN') - ->execute(); + if(!count($moduleNames)){ + $this->deleteAllModules($cid); + + }else{ + $existingSubscriptions = $this->database + ->query("SELECT * FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) + ->fetchAll(); + + $moduleIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_module') + ->accessCheck(FALSE) + ->condition('field_module_machine_name', $moduleNames, 'IN') + ->execute(); + + $this->subscribeToNewModules($cid, $moduleIds, $existingSubscriptions); + $this->unsubscribeFromOldModules($cid, $moduleIds, $existingSubscriptions); + } - $this->subscribeToNewModules($cid, $moduleIds, $existingSubscriptions); - $this->unsubscribeFromOldModules($cid, $moduleIds, $existingSubscriptions); + } private function subscribeToNewModules($cid, $moduleIds, $existingSubscriptions){ @@ -67,7 +74,7 @@ private function subscribeToNewModules($cid, $moduleIds, $existingSubscriptions) foreach($moduleIds as $moduleId){ - $subscriptionExists = array_find($existingSubscriptions, function($existingSubscription){ + $subscriptionExists = array_find($existingSubscriptions, function($existingSubscription) use ($moduleId){ return $existingSubscription->module_id === $moduleId; }); @@ -89,19 +96,30 @@ private function unsubscribeFromOldModules($cid, $moduleIds, $existingSubscripti $moduleIdsToDelete = []; foreach($existingSubscriptions as $existingSubscription){ - $keepSubscription = array_find($moduleIds, function($moduleId){ + $keepSubscription = array_find($moduleIds, function($moduleId) use ($existingSubscription){ return $moduleId === $existingSubscription->module_id; }); if(!$keepSubscription){ $moduleIdsToDelete[] = $existingSubscription->module_id; } } + + if(count($moduleIdsToDelete)) { + $this->database + ->delete('telegram_subscriptions') + ->condition('module_id', $moduleIdsToDelete, 'IN') + ->execute(); + } + + + } + + private function deleteAllModules($cid){ $this->database ->delete('telegram_subscriptions') - ->condition('module_id', $moduleIdsToDelete, 'IN') + ->condition('chat_id', $cid) ->execute(); - } public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { @@ -120,9 +138,10 @@ public function post() { $cid = strval($params['userInfo']['id']); $moduleNames = $params['modules']; - $userCreated = $this->createUserIfNonExistent($cid); - $this->updateModulesList($cid, $moduleNames); + if(count($moduleNames)) { + $this->createUserIfNonExistent($cid); + } - return new ResourceResponse(['userCreated' => $userCreated]); + return new ResourceResponse(['message': 'success']); } } \ No newline at end of file From e5940e6b8f4374c1a7c79c9f2663fff023d35230 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 15:07:19 +0300 Subject: [PATCH 30/57] small fix to bot code --- wisetrout-do-bot/scenes/subscription-menu.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 55791ba..572dabd 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -78,18 +78,16 @@ scene.action('select_none', ctx => { scene.action("save", async ctx => { - const modulesToSubscribe = ctx.session.modules.length === ctx.session.selectedModules.length ? - null : - ctx.session.selectedModules; + await Promise.all([ ctx.reply('Subscribing to updates..'), - subscribe(ctx.from, modulesToSubscribe) + subscribe(ctx.from, ctx.session.selectedModules) ]); ctx.session.userInfo = { subscribed: true, modules: ctx.session.selectedModules } - delete ctx.session.selectedModule; + delete ctx.session.selectedModules; delete ctx.session.modules; delete ctx.session.menuMessageId; ctx.reply('Subscription successful!'); From f03d72c51e3e5273ca59780c1cd78dffd5af582e Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 15:08:00 +0300 Subject: [PATCH 31/57] updated status endpoint id --- .../wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php index f8075e8..a999899 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php @@ -10,7 +10,7 @@ * Provides a resource to pause/renew subscription to Telegram bot. * * @RestResource( - * id = "wisetrout_do_bot_unsubscribe", + * id = "wisetrout_do_bot_update_status", * label = @Translation("Telegram bot pause/renew subscription"), * uri_paths = { * "create" = "/api/telegram/update-status" From db0e37c71a435133f553e186505ec4a52a3e84be Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 9 Apr 2026 15:48:32 +0300 Subject: [PATCH 32/57] removed body from response for Telegram subscription and Telegram user info updates --- .../src/Plugin/rest/resource/Subscribe.php | 2 +- .../src/Plugin/rest/resource/UpdateStatus.php | 2 +- wisetrout-do-bot/api-calls/subscription.js | 16 ---------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 13ea19e..5c7cd27 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -142,6 +142,6 @@ public function post() { $this->createUserIfNonExistent($cid); } - return new ResourceResponse(['message': 'success']); + return new ResourceResponse(NULL, 204); } } \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php index a999899..f4603a1 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UpdateStatus.php @@ -46,6 +46,6 @@ public function post() { ->condition('chat_id', $cid) ->execute(); - return new ResourceResponse(['message' => 'success!']); + return new ResourceResponse(NULL, 204); } } \ No newline at end of file diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 66bc3b9..03cd98c 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -29,14 +29,6 @@ export async function subscribe(userInfo, modules){ console.log('Success!'); console.log(res); - - - const json = await res.json(); - - console.log('Data:'); - console.log(json); - - }catch(err){ console.log(err); @@ -69,14 +61,6 @@ export async function updateStatus(chatId, subscribed){ console.log('Success!'); console.log(res); - - const json = await res.json(); - - console.log('Data:'); - console.log(json); - - - }catch(err){ console.log(err); } From 3ef74213953a2ac231aff435f63e22d58692a589 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 14 Apr 2026 09:52:01 +0300 Subject: [PATCH 33/57] fix(Telegram bot module): readded update of subscriptions list on the subscribe endpoint --- .../wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index 5c7cd27..adf8f34 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -142,6 +142,8 @@ public function post() { $this->createUserIfNonExistent($cid); } + $this->updateModulesList($cid, $moduleNames); + return new ResourceResponse(NULL, 204); } } \ No newline at end of file From c5d32df8a9357ed0acb0a0e4865ffd95014686b2 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 14 Apr 2026 10:01:10 +0300 Subject: [PATCH 34/57] config update --- config/sync/rest.resource.wisetrout_do_bot_modules_list.yml | 2 +- config/sync/rest.resource.wisetrout_do_bot_subscribe.yml | 2 +- ...yml => rest.resource.wisetrout_do_bot_update_status.yml} | 6 +++--- config/sync/rest.resource.wisetrout_do_bot_user_info.yml | 2 +- config/sync/rest.resource.wisetrout_do_bot_wipeout.yml | 2 +- config/sync/user.role.telegram_bot.yml | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) rename config/sync/{rest.resource.wisetrout_do_bot_unsubscribe.yml => rest.resource.wisetrout_do_bot_update_status.yml} (65%) diff --git a/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml b/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml index 1235679..6597760 100644 --- a/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml +++ b/config/sync/rest.resource.wisetrout_do_bot_modules_list.yml @@ -1,4 +1,4 @@ -uuid: 058ac28b-3692-4441-bc1c-1f55faa85dfd +uuid: 848f710d-aa0c-441b-b326-3ec264a31f9f langcode: en status: true dependencies: diff --git a/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml b/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml index 88da362..bd708b0 100644 --- a/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml +++ b/config/sync/rest.resource.wisetrout_do_bot_subscribe.yml @@ -1,4 +1,4 @@ -uuid: 183a38d1-bc4f-42bc-a5ea-a4882959bf60 +uuid: caaaf904-1827-43ab-8d7a-fbc38238e6e7 langcode: en status: true dependencies: diff --git a/config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml b/config/sync/rest.resource.wisetrout_do_bot_update_status.yml similarity index 65% rename from config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml rename to config/sync/rest.resource.wisetrout_do_bot_update_status.yml index 5f77a46..6dbfb36 100644 --- a/config/sync/rest.resource.wisetrout_do_bot_unsubscribe.yml +++ b/config/sync/rest.resource.wisetrout_do_bot_update_status.yml @@ -1,4 +1,4 @@ -uuid: 2b20c227-5dd8-4901-bb5e-9f1fb3979f31 +uuid: 43d6d24e-9dc0-4bf0-9c2f-077347c4caa2 langcode: en status: true dependencies: @@ -6,8 +6,8 @@ dependencies: - serialization - simple_oauth - wisetrout_do_bot -id: wisetrout_do_bot_unsubscribe -plugin_id: wisetrout_do_bot_unsubscribe +id: wisetrout_do_bot_update_status +plugin_id: wisetrout_do_bot_update_status granularity: resource configuration: methods: diff --git a/config/sync/rest.resource.wisetrout_do_bot_user_info.yml b/config/sync/rest.resource.wisetrout_do_bot_user_info.yml index 0b2ea52..e388bf2 100644 --- a/config/sync/rest.resource.wisetrout_do_bot_user_info.yml +++ b/config/sync/rest.resource.wisetrout_do_bot_user_info.yml @@ -1,4 +1,4 @@ -uuid: c7bfac03-2011-4acf-a043-af30ac97cece +uuid: a73f08f2-baf9-4e81-8a29-8d167105a661 langcode: en status: true dependencies: diff --git a/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml b/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml index 895c78a..17f3678 100644 --- a/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml +++ b/config/sync/rest.resource.wisetrout_do_bot_wipeout.yml @@ -1,4 +1,4 @@ -uuid: 6eb95e26-faa7-49a0-9f18-bed9b7f9ad14 +uuid: b193610a-e930-4ee6-9a4e-6c60e46283e4 langcode: en status: true dependencies: diff --git a/config/sync/user.role.telegram_bot.yml b/config/sync/user.role.telegram_bot.yml index 51daeef..5d69971 100644 --- a/config/sync/user.role.telegram_bot.yml +++ b/config/sync/user.role.telegram_bot.yml @@ -5,7 +5,7 @@ dependencies: config: - rest.resource.wisetrout_do_bot_modules_list - rest.resource.wisetrout_do_bot_subscribe - - rest.resource.wisetrout_do_bot_unsubscribe + - rest.resource.wisetrout_do_bot_update_status - rest.resource.wisetrout_do_bot_user_info - rest.resource.wisetrout_do_bot_wipeout module: @@ -18,5 +18,5 @@ permissions: - 'restful get wisetrout_do_bot_modules_list' - 'restful get wisetrout_do_bot_user_info' - 'restful post wisetrout_do_bot_subscribe' - - 'restful post wisetrout_do_bot_unsubscribe' + - 'restful post wisetrout_do_bot_update_status' - 'restful post wisetrout_do_bot_wipeout' From aa3d234d4b6c584fe036c8fb43f35d217c155e02 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 15 Apr 2026 15:57:21 +0300 Subject: [PATCH 35/57] feat(tg bot): sending messages to users about new/updated issues and new modules --- .env.example | 1 + composer.json | 3 +- composer.lock | 223 +++++++++++++++++- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 126 ++++++++++ 4 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d13ff4c --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +BOT_TOKEN=123 \ No newline at end of file diff --git a/composer.json b/composer.json index dd5f99d..a5cf6de 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "drupal/restui": "^1.22", "drupal/simple_oauth": "^6.1", "drupal/webform": "@beta", - "drush/drush": "^13" + "drush/drush": "^13", + "vlucas/phpdotenv": "^5.6" }, "conflict": { "drupal/drupal": "*" diff --git a/composer.lock b/composer.lock index 6160d61..51ba42b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f203bbc55635088f13b728769ae9430", + "content-hash": "76f95f39ca716879dd01db9deb4ffdc5", "packages": [ { "name": "asm89/stack-cors", @@ -8306,6 +8306,68 @@ ], "time": "2018-01-19T23:11:36+00:00" }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, { "name": "grasmash/expander", "version": "3.0.1", @@ -11010,6 +11072,81 @@ }, "time": "2025-04-21T15:27:20+00:00" }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, { "name": "phpowermove/docblock", "version": "v4.0", @@ -14804,6 +14941,90 @@ ], "time": "2025-05-03T07:21:55+00:00" }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php new file mode 100644 index 0000000..e63008a --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -0,0 +1,126 @@ +isPublished()){ + if ($node->getType() === 'ai_issue') { + $this->notifyAboutIssueCreation($node); + } + if ($node->getType() === 'ai_module') { + $this->notifyAboutModuleCreation($node); + } + } + } + + #[Hook('node_update')] + public function onNodeUpdate(NodeInterface $node): void { + if ($node->isPublished() && $node->getType() === 'ai_issue'){ + if($node->original->isPublished()) { + $this->notifyAboutIssueUpdate($node); + }else { + $this->notifyAboutIssueCreation($node); + } + } + + if ($node->isPublished() && $node->getType() === 'ai_module' && !($node->original->isPublished())){ + $this->notifyAboutModuleCreation($node); + } + } + + protected function notifyAboutIssueCreation(NodeInterface $node): void{ + + $chatIds = $this->getModuleChatIds($node); + + $message = '🌱New issue created:' + . $node->label() + ; + + + $this->sendBotNotifications($chatIds, $message); + + } + protected function notifyAboutIssueUpdate(NodeInterface $node): void{ + + $chatIds = $this->getModuleChatIds($node); + + $message = '✏️Issue updated:' + . $node->label() + ; + + + $this->sendBotNotifications($chatIds, $message); + } + + protected function notifyAboutModuleCreation(NodeInterface $node): void{ + $activeUserIds = $this->getActiveUserIds(); + $message = '🏷️New module created:' + . $node->label() + ; + $this->sendBotNotifications($activeUserIds, $message); + } + + protected function getModuleChatIds($node){ + $moduleEntity = $node->get('field_issue_module')->entity; + $moduleId = $moduleEntity->id(); + $db = \Drupal::database(); + $chatIds = $db + ->query("SELECT chat_id FROM {telegram_subscriptions} WHERE module_id = :module_id", [ + ':module_id' => $moduleId, + ]) + ->fetchCol(); + $activeChatIds = $db + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1", [ + ':chat_ids[]' => $chatIds, + ]) + ->fetchCol(); + return $activeChatIds; + } + + protected function getActiveUserIds(){ + $db = \Drupal::database(); + $activeUserIds = $db + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1") + ->fetchCol(); + return $activeUserIds; + } + + protected function sendBotNotifications($chatIds, $message){ + + + $httpClient = \Drupal::httpClient(); + + foreach($chatIds as $chatId){ + + $url = 'https://api.telegram.org/bot' + . $_ENV['BOT_TOKEN'] + . '/sendMessage'; + $payload = [ + 'chat_id' => $chatId, + 'text' => $message, + "parse_mode" => "HTML", + ]; + + $response = $httpClient + ->post($url, [ + 'body' => json_encode($payload), + 'headers' => [ + 'Content-Type' => 'application/json', + ] + ]); + } + } +} \ No newline at end of file From 97aa71b833c3623587c4e9c894ea4e13387d8d15 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 16 Apr 2026 13:55:33 +0300 Subject: [PATCH 36/57] feat(Telegram bot): added project link to issue creation notification --- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index e63008a..85e8c82 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -45,9 +45,12 @@ protected function notifyAboutIssueCreation(NodeInterface $node): void{ $chatIds = $this->getModuleChatIds($node); - $message = '🌱New issue created:' - . $node->label() - ; + $moduleName = $node->get('field_issue_module')->entity->label(); + + $authorName = $node->getOwner()->getDisplayName(); + + $message = "🌱 New issue created in {$moduleName} by {$authorName}: {$node->label()} + project URL: {$node->get('field_issue_url')->uri}"; $this->sendBotNotifications($chatIds, $message); @@ -57,9 +60,7 @@ protected function notifyAboutIssueUpdate(NodeInterface $node): void{ $chatIds = $this->getModuleChatIds($node); - $message = '✏️Issue updated:' - . $node->label() - ; + $message = "✏️Issue updated: {$node->label()}"; $this->sendBotNotifications($chatIds, $message); @@ -67,8 +68,7 @@ protected function notifyAboutIssueUpdate(NodeInterface $node): void{ protected function notifyAboutModuleCreation(NodeInterface $node): void{ $activeUserIds = $this->getActiveUserIds(); - $message = '🏷️New module created:' - . $node->label() + $message = "🏷️New module created:{$node->label()}"; ; $this->sendBotNotifications($activeUserIds, $message); } From 8983d858b72e3c6d0692fd84bc5754668884a4f7 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 16 Apr 2026 15:00:50 +0300 Subject: [PATCH 37/57] feat(telegram bot): showing list of updates to issue --- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index 85e8c82..e2b3b83 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -59,8 +59,17 @@ protected function notifyAboutIssueCreation(NodeInterface $node): void{ protected function notifyAboutIssueUpdate(NodeInterface $node): void{ $chatIds = $this->getModuleChatIds($node); + $updates = $this->findNodeChanges($node); - $message = "✏️Issue updated: {$node->label()}"; + if(!count($updates)) { + return; + } + + $message = "✏️Issue updated : {$node->label()}"; + + foreach($updates as $update){ + $message = $message . "\n{$update['title']}: {$update['old_value']} -> {$update['new_value']}"; + } $this->sendBotNotifications($chatIds, $message); @@ -98,6 +107,40 @@ protected function getActiveUserIds(){ return $activeUserIds; } + protected function findNodeChanges($node){ + $changedFields = []; + $original = $node->original; + foreach ($node->getFieldDefinitions() as $fieldName => $fieldDefinition) { + + if (str_starts_with($fieldName, 'field_') && !$node->get($fieldName)->equals($original->get($fieldName))) { + + $changedFields[] = [ + 'title' => (string) $fieldDefinition->getLabel(), + 'old_value' => $this->convertFieldToString($original, $fieldName), + 'new_value' => $this->convertFieldToString($node, $fieldName), + ]; + } + } + + return $changedFields; + } + + protected function convertFieldToString($node, $fieldName){ + $field = $node->get($fieldName); + + if ($field->isEmpty()) { + return ''; + } + + $values = []; + foreach ($field as $item) { + $itemValues = $item->getValue(); + $values[] = $itemValues['value'] ?? $item_values['target_id'] ?? (string) reset($itemValues); + } + + return implode(', ', $values); + } + protected function sendBotNotifications($chatIds, $message){ From 81319032f1e0cd82b4d433b54f4fabf6ee4953cd Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 16 Apr 2026 15:02:22 +0300 Subject: [PATCH 38/57] small tweak to bot message --- web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index e2b3b83..7525e40 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -77,7 +77,7 @@ protected function notifyAboutIssueUpdate(NodeInterface $node): void{ protected function notifyAboutModuleCreation(NodeInterface $node): void{ $activeUserIds = $this->getActiveUserIds(); - $message = "🏷️New module created:{$node->label()}"; + $message = "🏷️New module created: {$node->label()}"; ; $this->sendBotNotifications($activeUserIds, $message); } From 5575b7c97a0678a92b42a0bc12498d60826fc031 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Fri, 17 Apr 2026 10:07:02 +0300 Subject: [PATCH 39/57] feat(tg bot): access token refresh --- wisetrout-do-bot/api-calls/modules-list.js | 10 ++--- wisetrout-do-bot/api-calls/oauth.js | 9 ++--- wisetrout-do-bot/oauth.js | 43 +++++++++++++++++----- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/wisetrout-do-bot/api-calls/modules-list.js b/wisetrout-do-bot/api-calls/modules-list.js index ee26842..55b4d60 100644 --- a/wisetrout-do-bot/api-calls/modules-list.js +++ b/wisetrout-do-bot/api-calls/modules-list.js @@ -1,15 +1,11 @@ -import { oAuthToken } from "../oauth.js"; +import { sendAuthorizedRequest } from "../oauth.js"; export async function getModulesList(){ - const response = await fetch(process.env.BASE_URL + '/api/telegram/modules-list', { + + const response = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/modules-list', { method: 'GET', - headers: { - 'Authorization': `Bearer ${oAuthToken}` - } }) - if(!response.ok) throw new Error(response.status); - const modules = await response.json(); modules.sort(); diff --git a/wisetrout-do-bot/api-calls/oauth.js b/wisetrout-do-bot/api-calls/oauth.js index 674e0a6..a167e5c 100644 --- a/wisetrout-do-bot/api-calls/oauth.js +++ b/wisetrout-do-bot/api-calls/oauth.js @@ -1,8 +1,7 @@ export async function getOAuthToken(){ - const data = { - grant_type: process.env.DRUPAL_AUTH_TOKEN_GRANT_TYPE || '', + grant_type: 'client_credentials', client_id: process.env.DRUPAL_AUTH_TOKEN_CLIENT_ID || '', client_secret: process.env.DRUPAL_AUTH_TOKEN_CLIENT_SECRET || '', scope: process.env.DRUPAL_AUTH_TOKEN_SCOPE || '', @@ -18,11 +17,9 @@ export async function getOAuthToken(){ if(!res.ok) throw new Error(res.status); - const json = await res.json(); - - const token = json.access_token; + const responseData = await res.json(); - return token; + return responseData; }catch(err){ console.log(err); diff --git a/wisetrout-do-bot/oauth.js b/wisetrout-do-bot/oauth.js index 4d59b77..e480862 100644 --- a/wisetrout-do-bot/oauth.js +++ b/wisetrout-do-bot/oauth.js @@ -1,18 +1,41 @@ - -const TOKEN_EXPIRATION_MS = 5 * 60 * 1000; - import { getOAuthToken } from "./api-calls/oauth.js"; -export let oAuthToken = null; +let oAuthToken = null; export async function activateOAuthToken(){ - oAuthToken = await getOAuthToken(); - - setInterval(async function(){ - oAuthToken = await getOAuthToken(); - }, TOKEN_EXPIRATION_MS); + const responseData = await getOAuthToken(); - return oAuthToken; + oAuthToken = responseData.access_token; +} + +export async function sendAuthorizedRequest(url, { method, body, headers }){ + + const options = { method }; + + if(body) options.body = body; + options.headers = headers ? + {...headers, 'Authorization': `Bearer ${oAuthToken}`} : + {'Authorization': `Bearer ${oAuthToken}`}; + + try{ + const response = await fetch(url, options); + + if(response.status === 401){ + console.log('Token expired, getting new...'); + + const oauthData = await getOAuthToken(); + oAuthToken = oauthData.access_token; + const newResponse = await sendAuthorizedRequest(url, {method, body}); + return newResponse; + } + + if(!response.ok) throw new Error(response.status); + + return response; + + }catch(err){ + console.log(`Could not access ${url}: ${err}`); + } } \ No newline at end of file From bc36425db869589820bd9abfd7d0c04ea5ca47c2 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Fri, 17 Apr 2026 10:08:25 +0300 Subject: [PATCH 40/57] added .env.example --- wisetrout-do-bot/.env.example | 6 ++ wisetrout-do-bot/api-calls/subscription.js | 117 ++++++--------------- 2 files changed, 38 insertions(+), 85 deletions(-) create mode 100644 wisetrout-do-bot/.env.example diff --git a/wisetrout-do-bot/.env.example b/wisetrout-do-bot/.env.example new file mode 100644 index 0000000..d399cfa --- /dev/null +++ b/wisetrout-do-bot/.env.example @@ -0,0 +1,6 @@ +BOT_TOKEN=123 +BASE_URL='http://ddev-drupal-ai-dev-tracker-web:80' +DRUPAL_AUTH_TOKEN_CLIENT_ID='client_id' +DRUPAL_AUTH_TOKEN_CLIENT_SECRET='secretysecret' +DRUPAL_AUTH_TOKEN_SCOPE='token_scope' +PREFERENCES_ENDPOINT_BASE_URL='http://ddev-drupal-ai-dev-tracker-web:80' diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 03cd98c..5dcd771 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -1,4 +1,4 @@ -import { oAuthToken } from "../oauth.js"; +import { sendAuthorizedRequest } from "../oauth.js"; export async function subscribe(userInfo, modules){ console.log('Creating subscription...'); @@ -6,34 +6,22 @@ export async function subscribe(userInfo, modules){ console.log('Modules:'); console.log(modules); - const body = JSON.stringify(modules ? - {userInfo, modules} : - {userInfo}); + const body = JSON.stringify({userInfo, modules}); console.log('Body:'); console.log(body); - - try{ - const res = await fetch(process.env.BASE_URL + '/api/telegram/subscribe', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${oAuthToken}`, - 'Content-Type': 'application/json' - }, - body - }) - - if(!res.ok) throw new Error(res.status); - - console.log('Success!'); - console.log(res); - - }catch(err){ - console.log(err); - return null; - } + const res = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body + }); + + console.log(res); + } export async function updateStatus(chatId, subscribed){ @@ -43,59 +31,36 @@ export async function updateStatus(chatId, subscribed){ console.log('Body:'); console.log(body); - - - try{ - - const res = await fetch(process.env.BASE_URL + '/api/telegram/update-status', { + + const res = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/update-status', { method: 'POST', headers: { - 'Authorization': `Bearer ${oAuthToken}`, 'Content-Type': 'application/json' }, body - }) - - if(!res.ok) throw new Error(res.status); + }); - console.log('Success!'); - console.log(res); - }catch(err){ - console.log(err); - } + console.log(res); + } export async function checkSubscription(chatId){ - try{ - - const res = await fetch(`${process.env.BASE_URL}/api/telegram/user-info/${chatId}`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${oAuthToken}` - } - }) - - if(!res.ok) throw new Error(res.status); + const res = await sendAuthorizedRequest(`${process.env.BASE_URL}/api/telegram/user-info/${chatId}`, { + method: 'GET' + }); - console.log('user Info success!'); - console.log(res); + console.log('user Info success!'); + console.log(res); - const data = await res.json(); + const data = await res.json(); - console.log('User info:'); - console.log(data); - - - return data; - - + console.log('User info:'); + console.log(data); + - }catch(err){ - console.log(err); - return null; - } + return data; } export async function wipeoutData(chatId){ @@ -105,33 +70,15 @@ export async function wipeoutData(chatId){ console.log('Body:'); console.log(body); - - - try{ - - const res = await fetch(process.env.BASE_URL + '/api/telegram/wipeout', { + + const res = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/wipeout', { method: 'POST', headers: { - 'Authorization': `Bearer ${oAuthToken}`, 'Content-Type': 'application/json' }, body - }) - - if(!res.ok) throw new Error(res.status); - - console.log('Success!'); - console.log(res); - - - const json = await res.json(); - - console.log('Data:'); - console.log(json); - - + }); - }catch(err){ - console.log(err); - } + console.log('Success!'); + console.log(res); } \ No newline at end of file From d66c2c9aa187061a22080c78b976e8c9f916a88e Mon Sep 17 00:00:00 2001 From: MxLourier Date: Fri, 17 Apr 2026 15:00:26 +0300 Subject: [PATCH 41/57] feat(tg-bot): began working on daily summaries --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 170 ++++++++++++++++++ .../QueueWorker/DailySummaryProcessor.php | 25 +++ 2 files changed, 195 insertions(+) create mode 100644 web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php create mode 100644 web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php new file mode 100644 index 0000000..d71426d --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -0,0 +1,170 @@ +getRequestTime(); + $last_run = \Drupal::state()->get('wisetrout_do_bot.cron_last_run', 0); + + // Check if today is a different day than the last run. + if (date('Y-m-d', $current_time) !== date('Y-m-d', $last_run)) { + + $chatIds = $this->getActiveDailySubscriberIds(); + $updates = $this->getIssueUpdatesData(); + $subscriptions = $this->getDailySubscriptions($chatIds); + $newModules = $this->getnewModules(); + + $moduleSummaryNotifications = $this->createModuleSummaryNotifications($subscriptions, $updates); + $moduleCreationNotifications = $this->createModuleCreationNotifications($chatIds, $newModules); + + // Fetch the queue service. + $queue = \Drupal::queue('telegram_bot_queue'); + + $items_to_process = array_merge($moduleCreationNotifications, $moduleCreationNotifications); + + foreach ($items_to_process as $item_data) { + // Create an item in the queue. + $queue->createItem($item_data); + } + + // Update the last run time. + \Drupal::state()->set('wisetrout_do_bot.cron_last_run', $current_time); + } + } + + protected function getActiveDailySubscriberIds(){ + return \Drupal::database() + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1 AND type = 'daily'") + ->fetchCol(); + } + + protected function getIssueUpdatesData(){ + + $createdIssuesData = $this->getCreatedIssuesData(); + $updatedIssuesData = $this->getUpdatedIssuesData(); + + return array_merge($updatedIssuesData, $createdIssuesData); + + } + + protected function getDailySubscriptions($chatIds){ + return \Drupal::database() + ->query("SELECT chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ + ':chat_ids[]' => $chatIds, + ]) + ->fetchAllAssoc(); + } + + protected function getNewModules(){ + $yesterday = strtotime('-1 day'); + $storage = \Drupal::entityTypeManager()->getStorage('node'); + $createdIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_module') + ->condition('created', $yesterday, '>=') + ->accessCheck(FALSE) + ->execute(); + + $createdModuleNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); + $createdModuleNames = array_map(function($node){ + return $node->field_module_machine_name[0]->value; + }, $createdModuleNodes); + return $createdModuleNames; + } + + protected function createModuleSummaryNotifications($subscriptions, $updates){ + $notifications = []; + + } + + protected function createModuleCreationNotifications(){} + + protected function getCreatedIssuesData(){ + $yesterday = strtotime('-1 day'); + $storage = \Drupal::entityTypeManager()->getStorage('node'); + $createdIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_issue') + ->condition('created', $yesterday, '>=') + ->accessCheck(FALSE) + ->execute(); + + $data = []; + + $createdIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); + + $data = array_map(function($issueNode){ + return [ + 'id' => $issueNode->id(), + 'name' => $issueNode->label(), + 'module' => $issueNode->->get('field_issue_module')->entity->id(), + 'action' => 'created', + ]; + }, $createdIssueNodes); + + return $data; + + } + + protected function getUpdatedIssuesData(){ + $yesterday = strtotime('-1 day'); + $storage = \Drupal::entityTypeManager()->getStorage('node'); + + $updatedIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_issue') + ->condition('changed', $yesterday, '>=') + ->condition('created', $yesterday, '<') + ->accessCheck(FALSE) + ->execute(); + + $data = []; + + $updatedIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); + + foreach ($updatedIssueNodes as $issueNode) { + $vids = $storage->revisionIds($issueNode); + if (count($vids) >= 2) { + $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); + $changes = getChangedFields($issueNode, $previousRev); + $data[] = [ + 'id' => $issueNode->id(), + 'name' => $issueNode->label(), + 'module' => $issueNode->->get('field_issue_module')->entity->id(), + 'action' => 'updated', + 'changes' => $changes, + ]; + } + } + + return $data; + } + + protected function getChangedFields($newNode, $oldNode){ + $changes = []; + + foreach ($newNode->getFields() as $fieldName => $fieldItem) { + if (!str_starts_with($filedName, 'field_')) continue; + + if (!$newNode->get($fieldName)->equals($oldNode->get($fieldName))) { + $changes[] = [ + 'name': $fieldName, + 'old': $oldNode->get($fieldName), + 'new': $newNode->get($fieldName), + ]; + } + } + + return $changes; + } +} \ No newline at end of file diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php new file mode 100644 index 0000000..a6d5680 --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php @@ -0,0 +1,25 @@ + Date: Wed, 22 Apr 2026 11:32:10 +0300 Subject: [PATCH 42/57] Error handling and code style --- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 107 +++++++++--------- .../src/Plugin/rest/resource/UserInfo.php | 22 ++-- .../src/Plugin/rest/resource/Wipeout.php | 9 +- wisetrout-do-bot/api-calls/subscription.js | 30 +++-- wisetrout-do-bot/bot.js | 9 +- wisetrout-do-bot/compose.yaml | 13 ++- wisetrout-do-bot/oauth.js | 5 +- 7 files changed, 112 insertions(+), 83 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index 7525e40..7aa2b40 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -5,20 +5,23 @@ use Drupal\Core\Entity\EntityEvents; use Drupal\Core\Hook\Attribute\Hook; use Drupal\node\NodeInterface; +use GuzzleHttp\Exception\RequestException; /** * Hook implementations for wisetrout_do_bot. */ class NodeHooks { + const MAX_MESSAGE_LENGTH = 4096; + /** * Reacts to node insertion. */ #[Hook('node_insert')] public function onNodeInsert(NodeInterface $node): void { - if ($node->isPublished()){ + if ($node->isPublished()) { if ($node->getType() === 'ai_issue') { - $this->notifyAboutIssueCreation($node); + $this->notifyAboutIssueCreation($node); } if ($node->getType() === 'ai_module') { $this->notifyAboutModuleCreation($node); @@ -28,92 +31,88 @@ public function onNodeInsert(NodeInterface $node): void { #[Hook('node_update')] public function onNodeUpdate(NodeInterface $node): void { - if ($node->isPublished() && $node->getType() === 'ai_issue'){ - if($node->original->isPublished()) { + if ($node->isPublished() && $node->getType() === 'ai_issue') { + if ($node->original->isPublished()) { $this->notifyAboutIssueUpdate($node); - }else { + } + else { $this->notifyAboutIssueCreation($node); } } - if ($node->isPublished() && $node->getType() === 'ai_module' && !($node->original->isPublished())){ + if ($node->isPublished() && $node->getType() === 'ai_module' && !($node->original->isPublished())) { $this->notifyAboutModuleCreation($node); } } - protected function notifyAboutIssueCreation(NodeInterface $node): void{ - + protected function notifyAboutIssueCreation(NodeInterface $node): void { $chatIds = $this->getModuleChatIds($node); $moduleName = $node->get('field_issue_module')->entity->label(); $authorName = $node->getOwner()->getDisplayName(); - $message = "🌱 New issue created in {$moduleName} by {$authorName}: {$node->label()} + $label = strip_tags($node->label()); + $message = "🌱 New issue created in {$moduleName} by {$authorName}: $label project URL: {$node->get('field_issue_url')->uri}"; - $this->sendBotNotifications($chatIds, $message); - } - protected function notifyAboutIssueUpdate(NodeInterface $node): void{ + protected function notifyAboutIssueUpdate(NodeInterface $node): void { $chatIds = $this->getModuleChatIds($node); $updates = $this->findNodeChanges($node); - if(!count($updates)) { + if (!count($updates)) { return; } $message = "✏️Issue updated : {$node->label()}"; - foreach($updates as $update){ + foreach ($updates as $update) { $message = $message . "\n{$update['title']}: {$update['old_value']} -> {$update['new_value']}"; } - $this->sendBotNotifications($chatIds, $message); } - protected function notifyAboutModuleCreation(NodeInterface $node): void{ + protected function notifyAboutModuleCreation(NodeInterface $node): void { $activeUserIds = $this->getActiveUserIds(); - $message = "🏷️New module created: {$node->label()}"; - ; + $message = "🏷️New module created: {$node->label()}";; $this->sendBotNotifications($activeUserIds, $message); } - protected function getModuleChatIds($node){ + protected function getModuleChatIds($node) { $moduleEntity = $node->get('field_issue_module')->entity; $moduleId = $moduleEntity->id(); $db = \Drupal::database(); $chatIds = $db - ->query("SELECT chat_id FROM {telegram_subscriptions} WHERE module_id = :module_id", [ + ->query("SELECT chat_id FROM {telegram_subscriptions} WHERE module_id = :module_id", [ ':module_id' => $moduleId, - ]) - ->fetchCol(); + ]) + ->fetchCol(); $activeChatIds = $db - ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1", [ + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1", [ ':chat_ids[]' => $chatIds, - ]) - ->fetchCol(); + ]) + ->fetchCol(); return $activeChatIds; } - protected function getActiveUserIds(){ + protected function getActiveUserIds() { $db = \Drupal::database(); $activeUserIds = $db - ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1") - ->fetchCol(); + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1") + ->fetchCol(); return $activeUserIds; } - protected function findNodeChanges($node){ + protected function findNodeChanges($node) { $changedFields = []; $original = $node->original; foreach ($node->getFieldDefinitions() as $fieldName => $fieldDefinition) { - - if (str_starts_with($fieldName, 'field_') && !$node->get($fieldName)->equals($original->get($fieldName))) { - + if (str_starts_with($fieldName, 'field_') && !$node->get($fieldName) + ->equals($original->get($fieldName))) { $changedFields[] = [ 'title' => (string) $fieldDefinition->getLabel(), 'old_value' => $this->convertFieldToString($original, $fieldName), @@ -125,9 +124,9 @@ protected function findNodeChanges($node){ return $changedFields; } - protected function convertFieldToString($node, $fieldName){ + protected function convertFieldToString($node, $fieldName) { $field = $node->get($fieldName); - + if ($field->isEmpty()) { return ''; } @@ -135,35 +134,41 @@ protected function convertFieldToString($node, $fieldName){ $values = []; foreach ($field as $item) { $itemValues = $item->getValue(); - $values[] = $itemValues['value'] ?? $item_values['target_id'] ?? (string) reset($itemValues); + $values[] = strip_tags($itemValues['value'] ?? $item_values['target_id'] ?? (string) reset($itemValues)); } return implode(', ', $values); } - protected function sendBotNotifications($chatIds, $message){ - - + protected function sendBotNotifications($chatIds, $message) { $httpClient = \Drupal::httpClient(); - foreach($chatIds as $chatId){ - - $url = 'https://api.telegram.org/bot' - . $_ENV['BOT_TOKEN'] - . '/sendMessage'; + if (strlen($message) > self::MAX_MESSAGE_LENGTH) { + $message = substr($message, 0, self::MAX_MESSAGE_LENGTH - 4) . '...'; + } + + foreach ($chatIds as $chatId) { + $url = 'https://api.telegram.org/bot' + . getenv('BOT_TOKEN') + . '/sendMessage'; $payload = [ 'chat_id' => $chatId, 'text' => $message, "parse_mode" => "HTML", ]; - $response = $httpClient - ->post($url, [ - 'body' => json_encode($payload), - 'headers' => [ - 'Content-Type' => 'application/json', - ] - ]); + try { + $response = $httpClient->post($url, [ + 'body' => json_encode($payload), + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ]); + } + catch (RequestException $e) { + \Drupal::logger('wtbot')->warning($e->getMessage() . ': ' . $message); + } } } -} \ No newline at end of file + +} diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 737d95f..e2ef869 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -55,20 +55,18 @@ private function readModulesList($cid, $database){ public function get($cid){ $database = \Drupal::database(); - + $userStatus = $this->readUserStatus($cid, $database); - $responseData; - - if($userStatus === null) { - $responseData = null; - }else { - $modulesList = $this->readModulesList($cid, $database); - $responseData = [ - 'subscribed' => !!$userStatus, - 'modules' => $modulesList, - ]; + + $responseData = [ + 'subscribed' => !!$userStatus, + 'modules' => [], + ]; + + if ($userStatus !== null) { + $responseData['modules'] = $this->readModulesList($cid, $database); } return new ResourceResponse($responseData); } -} \ No newline at end of file +} diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php index 7d3966e..d793049 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Wipeout.php @@ -2,6 +2,7 @@ namespace Drupal\wisetrout_do_bot\Plugin\rest\resource; +use Drupal\Core\Database\Connection; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,6 +21,8 @@ class Wipeout extends ResourceBase { protected $currentRequest; + protected Connection $database; + private function deleteUserInfo($cid){ $this->database ->delete('telegram_subscribers') @@ -37,14 +40,14 @@ private function deleteUserSubscriptions($cid){ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); $instance->currentRequest = $container->get('request_stack')->getCurrentRequest(); - $instance->database = \Drupal::database(); + $instance->database = $container->get('database'); return $instance; } public function post() { $content = $this->currentRequest->getContent(); - + $params = json_decode($content, TRUE); $cid = $params['chatId']; @@ -54,4 +57,4 @@ public function post() { return new ResourceResponse(['message' => 'success!']); } -} \ No newline at end of file +} diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 5dcd771..f84958e 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -10,8 +10,8 @@ export async function subscribe(userInfo, modules){ console.log('Body:'); console.log(body); - - + + const res = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/subscribe', { method: 'POST', headers: { @@ -21,7 +21,7 @@ export async function subscribe(userInfo, modules){ }); console.log(res); - + } export async function updateStatus(chatId, subscribed){ @@ -40,9 +40,9 @@ export async function updateStatus(chatId, subscribed){ body }); - + console.log(res); - + } export async function checkSubscription(chatId){ @@ -52,13 +52,21 @@ export async function checkSubscription(chatId){ console.log('user Info success!'); console.log(res); - - - const data = await res.json(); + const text = await res.text(); + if (!text) { + throw new Error('Empty response from server'); + } + + let data; + try { + data = JSON.parse(text); + } catch (e) { + console.error('Failed to parse JSON. Response text was:', text); + throw new Error('Invalid JSON response from server'); + } console.log('User info:'); console.log(data); - return data; } @@ -70,7 +78,7 @@ export async function wipeoutData(chatId){ console.log('Body:'); console.log(body); - + const res = await sendAuthorizedRequest(process.env.BASE_URL + '/api/telegram/wipeout', { method: 'POST', headers: { @@ -81,4 +89,4 @@ export async function wipeoutData(chatId){ console.log('Success!'); console.log(res); -} \ No newline at end of file +} diff --git a/wisetrout-do-bot/bot.js b/wisetrout-do-bot/bot.js index 948fc42..bdcdf08 100644 --- a/wisetrout-do-bot/bot.js +++ b/wisetrout-do-bot/bot.js @@ -9,13 +9,14 @@ import { activateOAuthToken } from './oauth.js'; activateOAuthToken() .then(() => { + console.log('Oauth complete'); activateBot(); }); function activateBot(){ - + const bot = new Telegraf(process.env.BOT_TOKEN); bot.use(session()); @@ -28,7 +29,7 @@ function activateBot(){ bot.start(ctx => { ctx.reply( - 'Welcome to the Wisetrout Drupal org bot!', + 'Welcome to the Wisetrout Drupal org bot!', createActionsKeyboard(ctx) ) }); @@ -44,7 +45,7 @@ function activateBot(){ ]); ctx.session.userInfo.subscribed = false; - ctx.reply('Unsubscribed from all updates. We will keep your preferences saved in case you want to renew your subscription.', + ctx.reply('Unsubscribed from all updates. We will keep your preferences saved in case you want to renew your subscription.', createActionsKeyboard(ctx) ); }); @@ -57,7 +58,7 @@ function activateBot(){ ]); ctx.session.userInfo.subscribed = true; - ctx.reply('Subscription reactivated', + ctx.reply('Subscription reactivated', createActionsKeyboard(ctx) ); diff --git a/wisetrout-do-bot/compose.yaml b/wisetrout-do-bot/compose.yaml index 81bd251..8cefa43 100644 --- a/wisetrout-do-bot/compose.yaml +++ b/wisetrout-do-bot/compose.yaml @@ -5,7 +5,18 @@ services: context: . dockerfile: Dockerfile env_file: '.env' + extra_hosts: + - drupal-ai-dev-tracker.ddev.site=172.18.0.3 volumes: - type: bind source: . - target: /app \ No newline at end of file + target: /app + networks: +# ddev-drupal-ai-dev-tracker_default: {} + ddev_default: {} + +networks: +# ddev-drupal-ai-dev-tracker_default: +# external: true + ddev_default: + external: true diff --git a/wisetrout-do-bot/oauth.js b/wisetrout-do-bot/oauth.js index e480862..21aa71b 100644 --- a/wisetrout-do-bot/oauth.js +++ b/wisetrout-do-bot/oauth.js @@ -31,7 +31,10 @@ export async function sendAuthorizedRequest(url, { method, body, headers }){ return newResponse; } - if(!response.ok) throw new Error(response.status); + if(!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } return response; From bb2ff845bd2d0254b902cd66f2a647563f033595 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 27 Apr 2026 11:57:16 +0300 Subject: [PATCH 43/57] feat(tg-bot): created daily summary texts, added to queue on cron --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 187 ++++++++++++------ .../wisetrout_do_bot/wisetrout_do_bot.install | 20 ++ 2 files changed, 147 insertions(+), 60 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index d71426d..1df4e55 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -22,13 +22,20 @@ public function onCron(){ // Check if today is a different day than the last run. if (date('Y-m-d', $current_time) !== date('Y-m-d', $last_run)) { + + $creations = $this->getCreatedIssues(); + $updates = $this->getUpdatedIssues(); + + $moduleSummaries = $this->createModuleSummaries($updates, $creations); + $newModulesSummary = $this->createNewModulesSummary(); + $chatIds = $this->getActiveDailySubscriberIds(); - $updates = $this->getIssueUpdatesData(); + $subscriptions = $this->getDailySubscriptions($chatIds); - $newModules = $this->getnewModules(); + - $moduleSummaryNotifications = $this->createModuleSummaryNotifications($subscriptions, $updates); - $moduleCreationNotifications = $this->createModuleCreationNotifications($chatIds, $newModules); + $moduleSummaryNotifications = $this->createModuleSummaryNotifications($subscriptions, $moduleSummaries); + $moduleCreationNotifications = $this->createModuleCreationNotifications($chatIds, $newModulesSummary); // Fetch the queue service. $queue = \Drupal::queue('telegram_bot_queue'); @@ -51,15 +58,6 @@ protected function getActiveDailySubscriberIds(){ ->fetchCol(); } - protected function getIssueUpdatesData(){ - - $createdIssuesData = $this->getCreatedIssuesData(); - $updatedIssuesData = $this->getUpdatedIssuesData(); - - return array_merge($updatedIssuesData, $createdIssuesData); - - } - protected function getDailySubscriptions($chatIds){ return \Drupal::database() ->query("SELECT chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ @@ -68,30 +66,8 @@ protected function getDailySubscriptions($chatIds){ ->fetchAllAssoc(); } - protected function getNewModules(){ - $yesterday = strtotime('-1 day'); - $storage = \Drupal::entityTypeManager()->getStorage('node'); - $createdIds = \Drupal::entityQuery('node') - ->condition('type', 'ai_module') - ->condition('created', $yesterday, '>=') - ->accessCheck(FALSE) - ->execute(); - - $createdModuleNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); - $createdModuleNames = array_map(function($node){ - return $node->field_module_machine_name[0]->value; - }, $createdModuleNodes); - return $createdModuleNames; - } - - protected function createModuleSummaryNotifications($subscriptions, $updates){ - $notifications = []; - - } - protected function createModuleCreationNotifications(){} - - protected function getCreatedIssuesData(){ + protected function getCreatedIssues(){ $yesterday = strtotime('-1 day'); $storage = \Drupal::entityTypeManager()->getStorage('node'); $createdIds = \Drupal::entityQuery('node') @@ -104,20 +80,19 @@ protected function getCreatedIssuesData(){ $createdIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); - $data = array_map(function($issueNode){ - return [ - 'id' => $issueNode->id(), - 'name' => $issueNode->label(), - 'module' => $issueNode->->get('field_issue_module')->entity->id(), - 'action' => 'created', - ]; - }, $createdIssueNodes); + // $data = array_map(function($issueNode){ + // return [ + // 'node' => $issueNode, + // 'module' => $issueNode->->get('field_issue_module')->entity, + // 'action' => 'created', + // ]; + // }, $createdIssueNodes); - return $data; + return $createdIssueNodes; } - protected function getUpdatedIssuesData(){ + protected function getUpdatedIssues(){ $yesterday = strtotime('-1 day'); $storage = \Drupal::entityTypeManager()->getStorage('node'); @@ -132,24 +107,116 @@ protected function getUpdatedIssuesData(){ $updatedIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); - foreach ($updatedIssueNodes as $issueNode) { - $vids = $storage->revisionIds($issueNode); - if (count($vids) >= 2) { - $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); - $changes = getChangedFields($issueNode, $previousRev); - $data[] = [ - 'id' => $issueNode->id(), - 'name' => $issueNode->label(), - 'module' => $issueNode->->get('field_issue_module')->entity->id(), - 'action' => 'updated', - 'changes' => $changes, - ]; - } + // foreach ($updatedIssueNodes as $issueNode) { + // $vids = $storage->revisionIds($issueNode); + // if (count($vids) >= 2) { + // $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); + // $changes = getChangedFields($issueNode, $previousRev); + // $data[] = [ + // 'id' => $issueNode->id(), + // 'name' => $issueNode->label(), + // 'module' => $issueNode->->get('field_issue_module')->entity, + // 'action' => 'updated', + // 'changes' => $changes, + // ]; + // } + // } + + // return $data; + + return $updatedIssueNodes; + } + + + + protected function createModuleSummaries($updatedNodes, $createdNodes){ + $summaries = []; + $storage = \Drupal::entityTypeManager()->getStorage('node'); + + foreach($updatedNodes as $issueNode){ + $vids = $storage->revisionIds($issueNode); + if(count($vids) < 2) continue; + + $updateMessage = "\n✏️Issue updated : {$node->label()}:"; + + $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); + $changes = getChangedFields($issueNode, $previousRev); + + foreach($changes as $change){ + $changeMessage = "\n{$change['name']}: {$change['old']} -> {$change['new']}"; + $updateMessage = $updateMessage . $changeMessage; + } + + + $moduleId = $issueNnode->get('field_issue_module')->entity->id(); + + if($summaries[$moduleId]){ + $summaries[$moduleId] = $summaries[$moduleId] . $updateMessage; + } else{ + $summaries[$moduleId] = "🏷️${$issueNode->get('field_issue_module')->entity->label()} updates:" . $updateMessage; + } + } - return $data; + foreach($createdNodes as $issueNode){ + + $creationMessage = "\n🌱Issue created : {$node->label()}."; + + $moduleId = $issueNnode->get('field_issue_module')->entity->id(); + + if($summaries[$moduleId]){ + $summaries[$moduleId] = $summaries[$moduleId] . $creationMessage; + } else{ + $summaries[$moduleId] = "🏷️${$issueNode->get('field_issue_module')->entity->label()} updates:" . $creationMessage; + } + + } + + return $summaries; + } + protected function createNewModulesSummary(){ + $yesterday = strtotime('-1 day'); + $storage = \Drupal::entityTypeManager()->getStorage('node'); + $createdIds = \Drupal::entityQuery('node') + ->condition('type', 'ai_module') + ->condition('created', $yesterday, '>=') + ->accessCheck(FALSE) + ->execute(); + + $createdModuleNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); + $createdModuleNames = array_map(function($node){ + return $node->field_module_machine_name[0]->value; + }, $createdModuleNodes); + + $modulesList = join(', ', $createdModuleNames); + + $summary = "🏷️ New modules created: {$modulesList}."; + return $summary; + } + + protected function createModuleSummaryNotifications($subscriptions, $moduleSummaries){ + $notifications = array_map(function($subscription) use ($moduleSummaries){ + return [ + "chatId" => $subscription['chat_id'], + "message" => $moduleSummaries[$subscription['module_id']], + ]; + }, $subscriptions); + return $notifications; + } + + protected function createModuleCreationNotifications($chatIds, $summary){ + $notifications = array_map(function($chatId) use ($summary){ + return [ + "chatId" => $chatId, + "message" => $summary, + ]; + }, $chatIds); + return $notifications; + } + + protected function getChangedFields($newNode, $oldNode){ $changes = []; diff --git a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install index d3de12d..9597c6a 100644 --- a/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install +++ b/web/modules/custom/wisetrout_do_bot/wisetrout_do_bot.install @@ -45,6 +45,13 @@ function wisetrout_do_bot_schema() { 'length' => '10', 'not null' => TRUE, ], + 'type' => [ + 'description' => 'Subscription type (instant or daily)', + 'type' => 'varchar', + 'not null' => TRUE, + 'length' => 255, + 'default' => 'instant', + ], 'status' => [ 'description' => 'Status of subscription. 1 = active, 0 = inactive.', 'type' => 'int', @@ -65,3 +72,16 @@ function wisetrout_do_bot_schema() { return $schema; } +function wisetrout_do_bot_update_11001(){ + $schema = \Drupal::database()->schema(); + $spec = [ + 'description' => 'Subscription type (instant or daily)', + 'type' => 'varchar', + 'not null' => TRUE, + 'default' => 'instant', + 'length' => 255, + ]; + + $schema->addField('telegram_subscribers', 'type', $spec); +} + From 553a64ca6d5b943607b628d5eb9c887c2e0fec11 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 27 Apr 2026 12:28:30 +0300 Subject: [PATCH 44/57] feat(tg-bot): wrote queue worker for daily summaries --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 6 ++---- .../QueueWorker/DailySummaryProcessor.php | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index 1df4e55..43a8bb2 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -37,15 +37,13 @@ public function onCron(){ $moduleSummaryNotifications = $this->createModuleSummaryNotifications($subscriptions, $moduleSummaries); $moduleCreationNotifications = $this->createModuleCreationNotifications($chatIds, $newModulesSummary); - // Fetch the queue service. $queue = \Drupal::queue('telegram_bot_queue'); $items_to_process = array_merge($moduleCreationNotifications, $moduleCreationNotifications); foreach ($items_to_process as $item_data) { - // Create an item in the queue. - $queue->createItem($item_data); - } + $queue->createItem($item_data); + } // Update the last run time. \Drupal::state()->set('wisetrout_do_bot.cron_last_run', $current_time); diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php index a6d5680..430e45d 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/QueueWorker/DailySummaryProcessor.php @@ -5,7 +5,7 @@ use Drupal\Core\Queue\QueueWorkerBase; /** - * Processes intensive tasks for my_module. + * Processes intensive tasks wisetrout_do_bot * * @QueueWorker( * id = "telegram_bot_queue", @@ -19,7 +19,25 @@ class DailySummaryProcessor extends QueueWorkerBase { * {@inheritdoc} */ public function processItem($data) { + + $httpClient = \Drupal::httpClient(); + $url = 'https://api.telegram.org/bot' + . $_ENV['BOT_TOKEN'] + . '/sendMessage'; + $payload = [ + 'chat_id' => $data['chatId'], + 'text' => $data['message'], + "parse_mode" => "HTML", + ]; + + $response = $httpClient + ->post($url, [ + 'body' => json_encode($payload), + 'headers' => [ + 'Content-Type' => 'application/json', + ] + ]); } } \ No newline at end of file From f2f1d3a782eb025d042f5c677d4d1f625d53e5d0 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Tue, 28 Apr 2026 12:25:22 +0300 Subject: [PATCH 45/57] feat(tg-bot): updated bot menu to allow subscription type choice --- wisetrout-do-bot/api-calls/subscription.js | 7 +- wisetrout-do-bot/scenes/subscription-menu.js | 101 +++++++++++++------ 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 5dcd771..13b26f3 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -1,12 +1,12 @@ import { sendAuthorizedRequest } from "../oauth.js"; -export async function subscribe(userInfo, modules){ +export async function subscribe(userInfo, modules, type){ console.log('Creating subscription...'); console.log(userInfo); console.log('Modules:'); console.log(modules); - const body = JSON.stringify({userInfo, modules}); + const body = JSON.stringify({userInfo: {...userInfo, type}, modules}); console.log('Body:'); console.log(body); @@ -52,7 +52,8 @@ export async function checkSubscription(chatId){ console.log('user Info success!'); console.log(res); - + + if(res.status === 204) return null; const data = await res.json(); diff --git a/wisetrout-do-bot/scenes/subscription-menu.js b/wisetrout-do-bot/scenes/subscription-menu.js index 572dabd..e559698 100644 --- a/wisetrout-do-bot/scenes/subscription-menu.js +++ b/wisetrout-do-bot/scenes/subscription-menu.js @@ -3,39 +3,38 @@ import { getModulesList } from "../api-calls/modules-list.js"; import { subscribe } from "../api-calls/subscription.js"; const MODULES_PER_PAGE = 10; +const cancelBtn = Markup.button.callback("Cancel", "cancel"); export const scene = new Scenes.BaseScene('subscription-menu'); scene.enter(async ctx => { - try{ - const [modules] = await Promise.all([ - getModulesList(), - ctx.reply("Loading...") - ]); - - - ctx.session.modules = modules; - ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; - ctx.session.page = 0; + ctx.reply(`Pick subscription type: + "instant" will send you notifications immediately when updates happen + "Daily" will once a day send you a summary of all changes related to modules you watch`, Markup.inlineKeyboard([ + [ + Markup.button.callback("⚑Instant", "instant"), + Markup.button.callback("β˜€οΈ Daily", "daily") + ], + [cancelBtn] + ])); - await ctx.reply('Please pick the modules you are interested in and click "subscribe"'); +}); - const messageData = await ctx.reply(createModulesList(ctx), Markup.inlineKeyboard(createMenuRows(ctx))); +scene.action("instant", ctx => { + ctx.session.type = 'instant'; + offerModulesList(ctx); - const { message_id } = messageData; - +}) - ctx.session.menuMessageId = message_id; - - }catch(err){ - ctx.reply('Error'); - ctx.reply(err); - } +scene.action("daily", ctx => { + ctx.session.type = 'daily'; + offerModulesList(ctx); - }) + + scene.action("page_back", ctx => { ctx.session.page--; refreshMenu(ctx); @@ -50,9 +49,9 @@ scene.action("page_next", ctx => { scene.command(/.+/, async ctx => { const { command } = ctx; - if(['page_back', 'page_next', 'save', 'nothing', 'select_all', 'select_none'].includes(command)) return; + console.log('Command triggered'); + console.log(command); - if(ctx.session.selectedModules.includes(command)){ ctx.session.selectedModules = ctx.session.selectedModules.filter(m => m != command); }else{ @@ -81,19 +80,51 @@ scene.action("save", async ctx => { await Promise.all([ ctx.reply('Subscribing to updates..'), - subscribe(ctx.from, ctx.session.selectedModules) + subscribe(ctx.from, ctx.session.selectedModules, ctx.session.type) ]); ctx.session.userInfo = { subscribed: true, - modules: ctx.session.selectedModules + modules: ctx.session.selectedModules, + type: ctx.session.type } - delete ctx.session.selectedModules; - delete ctx.session.modules; - delete ctx.session.menuMessageId; + clearSceneData(ctx); ctx.reply('Subscription successful!'); ctx.scene.leave(); }) +scene.action("cancel", async ctx => { + clearSceneData(ctx); + ctx.reply('Change cancelled'); + ctx.scene.leave(); +}) + +async function offerModulesList(ctx){ + try{ + const [modules] = await Promise.all([ + getModulesList(), + ctx.reply("Loading...") + ]); + + + ctx.session.modules = modules; + ctx.session.selectedModules = ctx.session.userInfo ? ctx.session.userInfo.modules : []; + ctx.session.page = 0; + + await ctx.reply('Please pick the modules you are interested in and click "subscribe"'); + + const messageData = await ctx.reply(createModulesList(ctx), Markup.inlineKeyboard(createMenuRows(ctx))); + + const { message_id } = messageData; + + + ctx.session.menuMessageId = message_id; + + }catch(err){ + ctx.reply('Error'); + ctx.reply(err); + } +} + function createModulesList(ctx){ let message = ''; const firstModuleIndex = ctx.session.page * MODULES_PER_PAGE; @@ -136,7 +167,8 @@ function createMenuRows(ctx){ backBtn, Markup.button.callback("βœ”οΈ Save", "save"), forwardButton - ] + ], + [cancelBtn] ]; } @@ -153,4 +185,13 @@ function refreshMenu(ctx){ } -} \ No newline at end of file +} + +function clearSceneData(ctx){ + delete ctx.session.selectedModules; + delete ctx.session.modules; + delete ctx.session.menuMessageId; + delete ctx.session.page; + delete ctx.session.type; +} + From adfc75aa1a6f063ae06d6bb34f1016300bda5a78 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 29 Apr 2026 09:57:43 +0300 Subject: [PATCH 46/57] feat(tg-bot): reworked code to account for new "type" field of subscribers table, basic functionality of daily summaries done --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 157 ++++++++++-------- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 4 +- .../src/Plugin/rest/resource/Subscribe.php | 32 ++-- .../src/Plugin/rest/resource/UserInfo.php | 46 ++--- 4 files changed, 124 insertions(+), 115 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index 43a8bb2..4353425 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -35,19 +35,21 @@ public function onCron(){ $moduleSummaryNotifications = $this->createModuleSummaryNotifications($subscriptions, $moduleSummaries); - $moduleCreationNotifications = $this->createModuleCreationNotifications($chatIds, $newModulesSummary); - + $queue = \Drupal::queue('telegram_bot_queue'); - $items_to_process = array_merge($moduleCreationNotifications, $moduleCreationNotifications); + $items_to_process = $newModulesSummary ? + array_merge($moduleSummaryNotifications, $this->createModuleCreationNotifications($chatIds, $newModulesSummary)): + $moduleSummaryNotifications; foreach ($items_to_process as $item_data) { $queue->createItem($item_data); } + } + // Update the last run time. \Drupal::state()->set('wisetrout_do_bot.cron_last_run', $current_time); - } } protected function getActiveDailySubscriberIds(){ @@ -58,10 +60,10 @@ protected function getActiveDailySubscriberIds(){ protected function getDailySubscriptions($chatIds){ return \Drupal::database() - ->query("SELECT chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ + ->query("SELECT id, chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ ':chat_ids[]' => $chatIds, ]) - ->fetchAllAssoc(); + ->fetchAll(); } @@ -76,15 +78,7 @@ protected function getCreatedIssues(){ $data = []; - $createdIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); - - // $data = array_map(function($issueNode){ - // return [ - // 'node' => $issueNode, - // 'module' => $issueNode->->get('field_issue_module')->entity, - // 'action' => 'created', - // ]; - // }, $createdIssueNodes); + $createdIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($createdIds); return $createdIssueNodes; @@ -103,24 +97,7 @@ protected function getUpdatedIssues(){ $data = []; - $updatedIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); - - // foreach ($updatedIssueNodes as $issueNode) { - // $vids = $storage->revisionIds($issueNode); - // if (count($vids) >= 2) { - // $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); - // $changes = getChangedFields($issueNode, $previousRev); - // $data[] = [ - // 'id' => $issueNode->id(), - // 'name' => $issueNode->label(), - // 'module' => $issueNode->->get('field_issue_module')->entity, - // 'action' => 'updated', - // 'changes' => $changes, - // ]; - // } - // } - - // return $data; + $updatedIssueNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($updatedIds); return $updatedIssueNodes; } @@ -130,46 +107,80 @@ protected function getUpdatedIssues(){ protected function createModuleSummaries($updatedNodes, $createdNodes){ $summaries = []; $storage = \Drupal::entityTypeManager()->getStorage('node'); + $moduleUpdates = []; - foreach($updatedNodes as $issueNode){ - $vids = $storage->revisionIds($issueNode); - if(count($vids) < 2) continue; + foreach($createdNodes as $createdNode){ - $updateMessage = "\n✏️Issue updated : {$node->label()}:"; + $moduleId = $createdNode->get('field_issue_module')->entity->id(); - $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); - $changes = getChangedFields($issueNode, $previousRev); - - foreach($changes as $change){ - $changeMessage = "\n{$change['name']}: {$change['old']} -> {$change['new']}"; - $updateMessage = $updateMessage . $changeMessage; + if($moduleUpdates[$moduleId]){ + $createdIssues = $moduleUpdates[$moduleId]['created']; + if($moduleUpdates[$moduleId]['created']){ + $moduleUpdates[$moduleId]['created'][] = $createdNode; + }else{ + $moduleUpdates[$moduleId]['created'] = [$createdNode]; + } + }else{ + $moduleUpdates[$moduleId] = [ + 'label' => $createdNode->get('field_issue_module')->entity->label(), + 'created' => [$createdNode], + ]; } + } + + foreach($updatedNodes as $updatedNode){ - - $moduleId = $issueNnode->get('field_issue_module')->entity->id(); + $moduleId = $updatedNode->get('field_issue_module')->entity->id(); - if($summaries[$moduleId]){ - $summaries[$moduleId] = $summaries[$moduleId] . $updateMessage; - } else{ - $summaries[$moduleId] = "🏷️${$issueNode->get('field_issue_module')->entity->label()} updates:" . $updateMessage; + if($moduleUpdates[$moduleId]){ + if($moduleUpdates[$moduleId]['updated']){ + $moduleUpdates[$moduleId]['updated'][] = $updatedNode; + }else{ + $moduleUpdates[$moduleId]['updated'] = [$updatedNode]; + } + }else{ + $moduleUpdates[$moduleId] = [ + 'label' => $updatedNode->get('field_issue_module')->entity->label(), + 'updated' => [$updatedNode], + ]; } } - foreach($createdNodes as $issueNode){ + foreach($moduleUpdates as $moduleId => $moduleInfo){ - $creationMessage = "\n🌱Issue created : {$node->label()}."; - - $moduleId = $issueNnode->get('field_issue_module')->entity->id(); + $text = "🏷️{$moduleInfo["label"]}: updates:\n"; - if($summaries[$moduleId]){ - $summaries[$moduleId] = $summaries[$moduleId] . $creationMessage; - } else{ - $summaries[$moduleId] = "🏷️${$issueNode->get('field_issue_module')->entity->label()} updates:" . $creationMessage; + if($moduleInfo['created']){ + $text .= "New issue(s):\n"; + foreach($moduleInfo['created'] as $createdNode){ + $text .= "🌱{$createdNode->label()}\n"; + } } - } + if($moduleInfo['updated']){ + $text .= "Issue updates: \n"; + foreach($moduleInfo['updated'] as $issueNode){ + + $vids = $storage->revisionIds($issueNode); + // if(count($vids) < 2) continue; + + $text .= "✏️{$issueNode->label()}:\n"; + $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); + $changes = $this->getChangedFields($issueNode, $previousRev); + + foreach($changes as $change){ + $text .= "{$change['name']}: {$change['old']} -> {$change['new']}\n"; + } + + } + } + + $summaries[$moduleId] = $text; + + } + return $summaries; } @@ -183,7 +194,9 @@ protected function createNewModulesSummary(){ ->accessCheck(FALSE) ->execute(); - $createdModuleNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($nids); + if(!count($createdIds)) return null; + + $createdModuleNodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($createdIds); $createdModuleNames = array_map(function($node){ return $node->field_module_machine_name[0]->value; }, $createdModuleNodes); @@ -195,12 +208,18 @@ protected function createNewModulesSummary(){ } protected function createModuleSummaryNotifications($subscriptions, $moduleSummaries){ - $notifications = array_map(function($subscription) use ($moduleSummaries){ - return [ - "chatId" => $subscription['chat_id'], - "message" => $moduleSummaries[$subscription['module_id']], - ]; - }, $subscriptions); + + $notifications = []; + + foreach($subscriptions as $subscription){ + if($moduleSummaries[$subscription->module_id]){ + $notifications[] = [ + "chatId" => $subscription->chat_id, + "message" => $moduleSummaries[$subscription->module_id], + ]; + } + } + return $notifications; } @@ -219,13 +238,13 @@ protected function getChangedFields($newNode, $oldNode){ $changes = []; foreach ($newNode->getFields() as $fieldName => $fieldItem) { - if (!str_starts_with($filedName, 'field_')) continue; + if (!str_starts_with($fieldName, 'field_')) continue; if (!$newNode->get($fieldName)->equals($oldNode->get($fieldName))) { $changes[] = [ - 'name': $fieldName, - 'old': $oldNode->get($fieldName), - 'new': $newNode->get($fieldName), + 'name' => $fieldItem->getFieldDefinition()->getLabel(), + 'old' => $oldNode->get($fieldName)->getValue()[0]['value'], + 'new' => $newNode->get($fieldName)->getValue()[0]['value'], ]; } } diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index 7525e40..f65509e 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -92,7 +92,7 @@ protected function getModuleChatIds($node){ ]) ->fetchCol(); $activeChatIds = $db - ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1", [ + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1 AND type = 'instant'", [ ':chat_ids[]' => $chatIds, ]) ->fetchCol(); @@ -102,7 +102,7 @@ protected function getModuleChatIds($node){ protected function getActiveUserIds(){ $db = \Drupal::database(); $activeUserIds = $db - ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1") + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1 AND type = 'instant'") ->fetchCol(); return $activeUserIds; } diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php index adf8f34..abb74fb 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/Subscribe.php @@ -22,24 +22,29 @@ class Subscribe extends ResourceBase { protected $currentRequest; private $database; - private function createUserIfNonExistent($cid){ - - $subscriberData = $this->database - ->query("SELECT * FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid]) + private function upsertUser($userInfo){ + + $subscriberData = $this->database + ->query("SELECT * FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $userInfo["id"]]) ->fetchField(); - if(!$subscriberData){ - $subsriberCreationResult = $this->database->insert('telegram_subscribers') + if($subscriberData){ + $this->database->update('telegram_subscribers') + ->condition('chat_id', $userInfo["id"]) + ->fields([ + 'type' => $userInfo["type"], + ]) + ->execute(); + }else{ + $this->database->insert('telegram_subscribers') ->fields([ - 'chat_id' => $cid, + 'chat_id' => $userInfo["id"], 'status' => 1, 'created' => $this->currentRequest->server->get('REQUEST_TIME'), + 'type' => $userInfo["type"], ]) ->execute(); - return TRUE; } - - return FALSE; } private function updateModulesList($cid, $moduleNames){ @@ -135,13 +140,10 @@ public function post() { $params = json_decode($content, TRUE); + $this->upsertUser($params['userInfo']); + $cid = strval($params['userInfo']['id']); $moduleNames = $params['modules']; - - if(count($moduleNames)) { - $this->createUserIfNonExistent($cid); - } - $this->updateModulesList($cid, $moduleNames); return new ResourceResponse(NULL, 204); diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 737d95f..7209abb 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -19,56 +19,44 @@ */ class UserInfo extends ResourceBase { - private function readUserStatus($cid, $database){ + private function readSubscriberInfo($cid, $database){ $queryResult = $database - ->query("SELECT status FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid]) + ->query("SELECT status, type FROM {telegram_subscribers} WHERE chat_id = :cid", [':cid' => $cid]) ->fetchAssoc(); - $status = $queryResult ? $queryResult['status'] : null; - return $status; + return $queryResult; } private function readModulesList($cid, $database){ - $dbRows = $database + $moduleIds = $database ->query("SELECT module_id FROM {telegram_subscriptions} WHERE chat_id = :cid", [':cid' => $cid]) - ->fetchAll(); - - $moduleIds = []; - - foreach($dbRows as $dbRow){ - $moduleIds[] = $dbRow->module_id; - } + ->fetchCol(); $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($moduleIds); - $modules = []; - - foreach($nodes as $node){ - $modules[] = $node->field_module_machine_name[0]->value; - } + $modules = array_map(function ($node){ + return $node->field_module_machine_name[0]->value; + }, $nodes); return $modules; - - // return $moduleIds; } public function get($cid){ $database = \Drupal::database(); - - $userStatus = $this->readUserStatus($cid, $database); - $responseData; - if($userStatus === null) { - $responseData = null; - }else { + $subscriberInfo = $this->readSubscriberInfo($cid, $database); + + if(!$subscriberInfo) { + return new ResourceResponse(null, 204); + }else{ $modulesList = $this->readModulesList($cid, $database); - $responseData = [ - 'subscribed' => !!$userStatus, + return new ResourceResponse([ + 'subscribed' => !!$subscriberInfo['status'], 'modules' => $modulesList, - ]; + 'type' => $subscriberInfo['type'], + ]); } - return new ResourceResponse($responseData); } } \ No newline at end of file From f644b4293845c08c777a1c02368b0126e7713b0a Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 29 Apr 2026 11:07:21 +0300 Subject: [PATCH 47/57] tg-bot: updated to show all daily updates to each issue, not just latest update --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index 4353425..365f63f 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -60,7 +60,7 @@ protected function getActiveDailySubscriberIds(){ protected function getDailySubscriptions($chatIds){ return \Drupal::database() - ->query("SELECT id, chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ + ->query("SELECT chat_id, module_id FROM {telegram_subscriptions} WHERE chat_id IN (:chat_ids[])", [ ':chat_ids[]' => $chatIds, ]) ->fetchAll(); @@ -162,13 +162,9 @@ protected function createModuleSummaries($updatedNodes, $createdNodes){ $text .= "Issue updates: \n"; foreach($moduleInfo['updated'] as $issueNode){ - $vids = $storage->revisionIds($issueNode); - // if(count($vids) < 2) continue; + $changes = $this->findDailyChanges($issueNode); $text .= "✏️{$issueNode->label()}:\n"; - - $previousRev = $storage->loadRevision(end(array_slice($vids, -2, 1))); - $changes = $this->getChangedFields($issueNode, $previousRev); foreach($changes as $change){ $text .= "{$change['name']}: {$change['old']} -> {$change['new']}\n"; @@ -233,6 +229,31 @@ protected function createModuleCreationNotifications($chatIds, $summary){ return $notifications; } + protected function findDailyChanges($node){ + + $storage = \Drupal::entityTypeManager()->getStorage('node'); + + $current_time = \Drupal::time()->getRequestTime(); + $yesterday = strtotime('-1 day'); + + $oldVid = $this->getRevisionBeforeTimestamp($node->id(), $yesterday); + $oldNode = $storage->loadRevision($oldVid); + + return $this->getChangedFields($node, $oldNode); + } + + protected function getRevisionBeforeTimestamp($nid, $timestamp) { + $database = \Drupal::database(); + return $database->select('node_revision', 'nr') + ->fields('nr', ['vid']) + ->condition('nid', $nid) + ->condition('revision_timestamp', $timestamp, '<') + ->orderBy('revision_timestamp', 'DESC') + ->range(0, 1) + ->execute() + ->fetchField(); + } + protected function getChangedFields($newNode, $oldNode){ $changes = []; From 03bd6283ee582102a21ea09c96e12f0b97b36499 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Wed, 29 Apr 2026 12:30:36 +0300 Subject: [PATCH 48/57] feat(tg-bot): handling various types of updated fields in issue nodes --- .../wisetrout_do_bot/src/Hook/CronHooks.php | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index 365f63f..9d2795d 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -149,7 +149,7 @@ protected function createModuleSummaries($updatedNodes, $createdNodes){ foreach($moduleUpdates as $moduleId => $moduleInfo){ - $text = "🏷️{$moduleInfo["label"]}: updates:\n"; + $text = "🏷️{$moduleInfo["label"]} updates:\n"; if($moduleInfo['created']){ $text .= "New issue(s):\n"; @@ -244,6 +244,8 @@ protected function findDailyChanges($node){ protected function getRevisionBeforeTimestamp($nid, $timestamp) { $database = \Drupal::database(); + $storage = \Drupal::entityTypeManager()->getStorage('node'); + return $database->select('node_revision', 'nr') ->fields('nr', ['vid']) ->condition('nid', $nid) @@ -257,19 +259,43 @@ protected function getRevisionBeforeTimestamp($nid, $timestamp) { protected function getChangedFields($newNode, $oldNode){ $changes = []; + $storage = \Drupal::entityTypeManager()->getStorage('node'); foreach ($newNode->getFields() as $fieldName => $fieldItem) { if (!str_starts_with($fieldName, 'field_')) continue; if (!$newNode->get($fieldName)->equals($oldNode->get($fieldName))) { + + $oldValue; + $newValue; + + $oldValue = $oldNode->get($fieldName)->getString(); + $newValue = $fieldItem->getString(); + + if($fieldItem->getFieldDefinition()->getType() === "entity_reference"){ + $oldValue = $this->convertIdsToLabels($oldValue, $storage); + $newValue = $this->convertIdsToLabels($newValue, $storage); + } + $changes[] = [ 'name' => $fieldItem->getFieldDefinition()->getLabel(), - 'old' => $oldNode->get($fieldName)->getValue()[0]['value'], - 'new' => $newNode->get($fieldName)->getValue()[0]['value'], + 'old' => $oldValue ? $oldValue : '(none)', + 'new' => $newValue ? $newValue : '(none)', ]; } } return $changes; } + + function convertIdsToLabels($string, $storage){ + if($string === "") return null; + $idsArray = explode(',', $string); + $nodesArray = $storage->loadMultiple($idsArray); + $nodeLabels = array_map(function($node){ + return $node->label(); + }, $nodesArray); + $labelsString = implode(', ', $nodeLabels); + return $labelsString; + } } \ No newline at end of file From 0ac95698a397cae8b7dd0189c0b714860cff602f Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 4 May 2026 09:27:53 +0000 Subject: [PATCH 49/57] added phpmyadmin --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba2c594..9f2d818 100644 --- a/.gitignore +++ b/.gitignore @@ -204,4 +204,8 @@ test-kanban-login.js # Ignore local development code scratchpad .code/ -private \ No newline at end of file +private +.ddev/docker-compose.phpmyadmin_norouter.yaml +.ddev/docker-compose.phpmyadmin.yaml +.ddev/addon-metadata/phpmyadmin/manifest.yaml +.ddev/commands/host/phpmyadmin From cb06f7346160a702a60d510e29d3dee1df82e818 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 4 May 2026 15:21:45 +0300 Subject: [PATCH 50/57] added phpmyadmin --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba2c594..a438143 100644 --- a/.gitignore +++ b/.gitignore @@ -204,4 +204,8 @@ test-kanban-login.js # Ignore local development code scratchpad .code/ -private \ No newline at end of file +private +.ddev/docker-compose.phpmyadmin_norouter.yaml +.ddev/docker-compose.phpmyadmin.yaml +.ddev/addon-metadata/phpmyadmin/manifest.yaml +.ddev/commands/host/phpmyadmin \ No newline at end of file From 252c57530e8825fe2fd8acc5cba469579b79bcb7 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Thu, 7 May 2026 08:58:45 +0000 Subject: [PATCH 51/57] fix(tg-bot): some errors with user info api requests --- .../wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php | 2 +- wisetrout-do-bot/api-calls/subscription.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 7b25055..6237b57 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -38,7 +38,7 @@ private function readModulesList($cid, $database){ return $node->field_module_machine_name[0]->value; }, $nodes); - return $modules; + return array_values($modules); } public function get($cid){ diff --git a/wisetrout-do-bot/api-calls/subscription.js b/wisetrout-do-bot/api-calls/subscription.js index 3350fca..2990818 100644 --- a/wisetrout-do-bot/api-calls/subscription.js +++ b/wisetrout-do-bot/api-calls/subscription.js @@ -57,9 +57,8 @@ export async function checkSubscription(chatId){ let data; try { - data = JSON.parse(text); + data = await res.json(); } catch (e) { - console.error('Failed to parse JSON. Response text was:', text); throw new Error('Invalid JSON response from server'); } From 24b61a0130576d526a7d8591e8659acab94b43df Mon Sep 17 00:00:00 2001 From: MxLourier Date: Fri, 29 May 2026 12:55:15 +0300 Subject: [PATCH 52/57] some minor fixes --- .../wisetrout_do_bot/src/Hook/NodeHooks.php | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index f65509e..c7f4405 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -5,18 +5,21 @@ use Drupal\Core\Entity\EntityEvents; use Drupal\Core\Hook\Attribute\Hook; use Drupal\node\NodeInterface; +use GuzzleHttp\Exception\RequestException; /** * Hook implementations for wisetrout_do_bot. */ class NodeHooks { + const MAX_MESSAGE_LENGTH = 4096; + /** * Reacts to node insertion. */ #[Hook('node_insert')] public function onNodeInsert(NodeInterface $node): void { - if ($node->isPublished()){ + if ($node->isPublished()) { if ($node->getType() === 'ai_issue') { $this->notifyAboutIssueCreation($node); } @@ -28,20 +31,20 @@ public function onNodeInsert(NodeInterface $node): void { #[Hook('node_update')] public function onNodeUpdate(NodeInterface $node): void { - if ($node->isPublished() && $node->getType() === 'ai_issue'){ - if($node->original->isPublished()) { + if ($node->isPublished() && $node->getType() === 'ai_issue') { + if ($node->original->isPublished()) { $this->notifyAboutIssueUpdate($node); - }else { + } else { $this->notifyAboutIssueCreation($node); } } - if ($node->isPublished() && $node->getType() === 'ai_module' && !($node->original->isPublished())){ + if ($node->isPublished() && $node->getType() === 'ai_module' && !($node->original->isPublished())) { $this->notifyAboutModuleCreation($node); } } - protected function notifyAboutIssueCreation(NodeInterface $node): void{ + protected function notifyAboutIssueCreation(NodeInterface $node): void { $chatIds = $this->getModuleChatIds($node); @@ -61,13 +64,13 @@ protected function notifyAboutIssueUpdate(NodeInterface $node): void{ $chatIds = $this->getModuleChatIds($node); $updates = $this->findNodeChanges($node); - if(!count($updates)) { + if (!count($updates)) { return; } $message = "✏️Issue updated : {$node->label()}"; - foreach($updates as $update){ + foreach ($updates as $update) { $message = $message . "\n{$update['title']}: {$update['old_value']} -> {$update['new_value']}"; } @@ -92,14 +95,14 @@ protected function getModuleChatIds($node){ ]) ->fetchCol(); $activeChatIds = $db - ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) and status = 1 AND type = 'instant'", [ + ->query("SELECT chat_id FROM {telegram_subscribers} WHERE chat_id IN (:chat_ids[]) AND status = 1 AND type = 'instant'", [ ':chat_ids[]' => $chatIds, ]) ->fetchCol(); return $activeChatIds; } - protected function getActiveUserIds(){ + protected function getActiveUserIds() { $db = \Drupal::database(); $activeUserIds = $db ->query("SELECT chat_id FROM {telegram_subscribers} WHERE status = 1 AND type = 'instant'") @@ -135,7 +138,7 @@ protected function convertFieldToString($node, $fieldName){ $values = []; foreach ($field as $item) { $itemValues = $item->getValue(); - $values[] = $itemValues['value'] ?? $item_values['target_id'] ?? (string) reset($itemValues); + $values[] = strip_tags($itemValues['value'] ?? $item_values['target_id'] ?? (string) reset($itemValues)); } return implode(', ', $values); @@ -145,11 +148,16 @@ protected function sendBotNotifications($chatIds, $message){ $httpClient = \Drupal::httpClient(); + $bot_token = \Drupal::service('settings')->get('tgbot_token'); + + if (strlen($message) > self::MAX_MESSAGE_LENGTH) { + $message = substr($message, 0, self::MAX_MESSAGE_LENGTH - 4) . '...'; + } foreach($chatIds as $chatId){ $url = 'https://api.telegram.org/bot' - . $_ENV['BOT_TOKEN'] + . $bot_token . '/sendMessage'; $payload = [ 'chat_id' => $chatId, @@ -157,13 +165,18 @@ protected function sendBotNotifications($chatIds, $message){ "parse_mode" => "HTML", ]; - $response = $httpClient - ->post($url, [ - 'body' => json_encode($payload), - 'headers' => [ - 'Content-Type' => 'application/json', - ] - ]); + try{ + $response = $httpClient + ->post($url, [ + 'body' => json_encode($payload), + 'headers' => [ + 'Content-Type' => 'application/json', + ] + ]); + }catch (RequestException $e) { + \Drupal::logger('wtbot')->warning($e->getMessage() . ': ' . $message); + } } + } } \ No newline at end of file From b2fcff0ef925c5b0b093c93cac7713bcc6d612c5 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Fri, 29 May 2026 13:16:17 +0300 Subject: [PATCH 53/57] fix(tg-bot): a couple small fixes --- .../custom/wisetrout_do_bot/src/Hook/CronHooks.php | 2 +- .../src/Plugin/rest/resource/UserInfo.php | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php index 9d2795d..27458f0 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/CronHooks.php @@ -208,7 +208,7 @@ protected function createModuleSummaryNotifications($subscriptions, $moduleSumma $notifications = []; foreach($subscriptions as $subscription){ - if($moduleSummaries[$subscription->module_id]){ + if(isset($moduleSummaries[$subscription->module_id])){ $notifications[] = [ "chatId" => $subscription->chat_id, "message" => $moduleSummaries[$subscription->module_id], diff --git a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php index 7209abb..f83b206 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php +++ b/web/modules/custom/wisetrout_do_bot/src/Plugin/rest/resource/UserInfo.php @@ -34,9 +34,11 @@ private function readModulesList($cid, $database){ $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadMultiple($moduleIds); - $modules = array_map(function ($node){ - return $node->field_module_machine_name[0]->value; - }, $nodes); + $modules = []; + + foreach($nodes as $node){ + $modules[] = $node->field_module_machine_name[0]->value; + } return $modules; } From 170b025fed06978d7bb9a539188256731e89361a Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 1 Jun 2026 12:44:23 +0300 Subject: [PATCH 54/57] Added Telegram bot setup instructions --- web/modules/custom/wisetrout_do_bot/README.md | 19 +++++++++++++++++++ wisetrout-do-bot/README.md | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 web/modules/custom/wisetrout_do_bot/README.md create mode 100644 wisetrout-do-bot/README.md diff --git a/web/modules/custom/wisetrout_do_bot/README.md b/web/modules/custom/wisetrout_do_bot/README.md new file mode 100644 index 0000000..65d437a --- /dev/null +++ b/web/modules/custom/wisetrout_do_bot/README.md @@ -0,0 +1,19 @@ +## DO bot custom module +This module creates the neccessary endpoints and hooks to be used with the DO Telegram bot. + +### Functionality +This module has endpoints for saving user Telegram preferences and hooks to notify users about new/updated issues and new modules. + +### Usage + +#### Bot token + +In order for the module to work, it must receive the bot token. This is the token you receive at Telegram bot creation (via BotFather bot). Save the token inside web\sites\default\settings.php, towards the bottom: + +``` +$settings['tgbot_token'] = '...'; // your token here +``` +This token must be the same as the one inside "wisetrout-do-bot\.env". + +#### Cron setup +Run cron regularly (once a day) to send out daily summaries to Telegram subscribers. \ No newline at end of file diff --git a/wisetrout-do-bot/README.md b/wisetrout-do-bot/README.md new file mode 100644 index 0000000..54bb41e --- /dev/null +++ b/wisetrout-do-bot/README.md @@ -0,0 +1,18 @@ +# Telegram bot + +The DO Telegram bot allows users to subscribe to AI Dashboard issue updates. They then receive notifications directly in the chat bot. There are two types of subscriptions: getting updates instantly on issue creation/update or receiving summaries once a day. This directory contains the code for the behavior of the Telegram bot, written in Node.js. It contains all of the commands and menus that the user sees once they interact with the bot in Telegram. + +## Usage +To make the bot work, several steps must be done: + +### Create bot user +Create a new user with the telegram_bot role. + +### Set up OAuth for the bot user +The bot uses OAuth to access the api and save user preferences. To set up Oauth, go to /admin/config/people/simple_oauth and set up the keys. After that you will need to create a consumer dedicated to the Telegram bot. Once this is done, you can fill up the DRUPAL_AUTH_TOKEN_CLIENT_ID, DRUPAL_AUTH_TOKEN_CLIENT_SECRET, DRUPAL_AUTH_TOKEN_SCOPE environment variables inside .env. + +### Create bot in Telegram +If you do not have one already, you must create a bot token via the BotFather bot in Telegram. Save the bot token as BOT_TOKEN variable in .env. + +### Restart project +The newly created environment variables will be passed to the bot and the bot will start working. From c59c64b3b8711729ba2f4dd333eb450c2397f948 Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 1 Jun 2026 13:12:17 +0300 Subject: [PATCH 55/57] Telegram bot: removed mention of author from issue creation notification --- web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php index 7da4736..c4f80cb 100644 --- a/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php +++ b/web/modules/custom/wisetrout_do_bot/src/Hook/NodeHooks.php @@ -51,10 +51,8 @@ protected function notifyAboutIssueCreation(NodeInterface $node): void { $moduleName = $node->get('field_issue_module')->entity->label(); - $authorName = $node->getOwner()->getDisplayName(); - $label = strip_tags($node->label()); - $message = "🌱 New issue created in {$moduleName} by {$authorName}: $label + $message = "🌱 New issue created in {$moduleName}: $label project URL: {$node->get('field_issue_url')->uri}"; $this->sendBotNotifications($chatIds, $message); From cbe45860adc28762169295a2b654a799cc7dbdcd Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 1 Jun 2026 13:13:00 +0300 Subject: [PATCH 56/57] Telegram bot: added OAuth token fetch retries --- wisetrout-do-bot/api-calls/oauth.js | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/wisetrout-do-bot/api-calls/oauth.js b/wisetrout-do-bot/api-calls/oauth.js index a167e5c..c35390f 100644 --- a/wisetrout-do-bot/api-calls/oauth.js +++ b/wisetrout-do-bot/api-calls/oauth.js @@ -1,3 +1,6 @@ +const OAUTH_MAX_ATTEMPTS = 5; +const RETRY_INTERVAL_MS = 10000; + export async function getOAuthToken(){ const data = { @@ -9,20 +12,30 @@ export async function getOAuthToken(){ const body = new URLSearchParams(data); - try{ - const res = await fetch(process.env.BASE_URL + '/oauth/token', { - method: 'POST', - body: body - }) + let attemptsDone = 0; + + do{ + attemptsDone++; + try{ + const res = await fetch(process.env.BASE_URL + '/oauth/token', { + method: 'POST', + body: body + }); + if(!res.ok) throw new Error(res.status); + const responseData = await res.json(); + return responseData; + }catch(err){ + console.log('Failed attempt No ' + attemptsDone); + console.log(err); + await new Promise(res => { + setTimeout(()=> {res()}, RETRY_INTERVAL_MS); + }); + continue; + } + - if(!res.ok) throw new Error(res.status); + }while(attempts_done < OAUTH_MAX_ATTEMPTS); - const responseData = await res.json(); - return responseData; - }catch(err){ - console.log(err); - return null; - } } \ No newline at end of file From 90b77d7fe5560fff4f13dc5060fc9d136ff9ccdf Mon Sep 17 00:00:00 2001 From: MxLourier Date: Mon, 1 Jun 2026 14:55:17 +0300 Subject: [PATCH 57/57] fixed small typo --- wisetrout-do-bot/api-calls/oauth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wisetrout-do-bot/api-calls/oauth.js b/wisetrout-do-bot/api-calls/oauth.js index c35390f..4a39eb5 100644 --- a/wisetrout-do-bot/api-calls/oauth.js +++ b/wisetrout-do-bot/api-calls/oauth.js @@ -34,7 +34,7 @@ export async function getOAuthToken(){ } - }while(attempts_done < OAUTH_MAX_ATTEMPTS); + }while(attemptsDone < OAUTH_MAX_ATTEMPTS);