From cb50697a61837e02f0e2c266855e39b3a5377f78 Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Sat, 10 May 2025 15:56:15 -0700 Subject: [PATCH 1/2] chore(so-bot): update project --- .github/dependabot.yml | 35 ++++++++++++ .github/workflows/notify.yml | 6 +- .nvmrc | 1 + LICENSE | 20 +++++++ README.md | 101 ++++++++++++++++++++++++++++++++++ package.json | 9 ++- src/index.ts | 29 +++++----- src/services/persistence.ts | 24 +++++--- src/services/stackoverflow.ts | 5 +- tsconfig.json | 6 +- 10 files changed, 200 insertions(+), 36 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .nvmrc create mode 100644 LICENSE create mode 100644 README.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a14c6f9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + time: "11:00" + open-pull-requests-limit: 10 + versioning-strategy: increase-if-necessary + groups: + patch-deps-updates-main: + update-types: + - "patch" + minor-deps-updates-main: + update-types: + - "minor" + major-deps-updates-main: + update-types: + - "major" +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "11:00" + open-pull-requests-limit: 10 + groups: + patch-deps-updates: + update-types: + - "patch" + minor-deps-updates: + update-types: + - "minor" + major-deps-updates: + update-types: + - "major" \ No newline at end of file diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index fdd12bb..9c2541b 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -13,16 +13,16 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Persist credentials so git push works persist-credentials: true fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '22' + node-version-file: .nvmrc - name: Install dependencies run: npm ci diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..d2c5c8a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.15.0 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4852cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) OpenJS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d633e47 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# WDIO Discord Bot + +A Node.js bot that bridges Stack Overflow and Discord for the WebdriverIO community. + +## Features + +- **Monitors Stack Overflow** for new questions tagged with a configurable tag (default: `webdriver-io`). +- **Posts new questions** as rich embeds to a specified Discord channel via webhook. +- **Prevents duplicate notifications** by persisting sent question IDs. +- **Configurable** via environment variables. +- **Easy to extend** for other tags or notification channels. + +## Getting Started + +### Prerequisites + +- Node.js v22+ (for native fetch and ESM support) +- npm +- Access to a Discord server where you can create a webhook + +### Installation + +1. **Clone the repository:** + ```sh + git clone + cd wdio-discord-bot + ``` + +2. **Install dependencies:** + ```sh + npm install + ``` + +3. **Configure environment variables:** + + Create a `.env` file in the project root with the following content: + + ``` + DISCORD_WEBHOOK_ID=your_discord_webhook_id + DISCORD_WEBHOOK_TOKEN=your_discord_webhook_token + TAG_TO_MONITOR=webdriver-io + # Optional: STACKEXCHANGE_KEY=your_stackexchange_api_key + ``` + + - `DISCORD_WEBHOOK_ID` and `DISCORD_WEBHOOK_TOKEN` are from your Discord channel webhook. + - `TAG_TO_MONITOR` is the Stack Overflow tag to monitor. + - `STACKEXCHANGE_KEY` is optional but recommended for higher API rate limits. + +4. **Run the bot:** + + - For development (with TypeScript): + ```sh + npm run dev + ``` + - For production: + ```sh + npm start + ``` + +## How It Works + +- On each run, the bot: + 1. Loads the list of previously notified question IDs from `data/sentIds.json`. + 2. Fetches the latest questions from Stack Overflow tagged with your configured tag. + 3. Filters out questions already sent. + 4. Formats and posts new questions to Discord as embeds. + 5. Updates `sentIds.json` and auto-commits/pushes the file (if using git). + +## Development Advice + +- **TypeScript:** The project is written in TypeScript. Use `npm run dev` for live development. +- **Persistence:** Sent question IDs are stored in `data/sentIds.json`. To reset notifications, delete or clear this file. +- **Auto-commit:** Each time new questions are sent, the bot auto-commits and pushes the updated `sentIds.json`. Ensure your environment has git configured and permissions set. +- **Extending:** To monitor a different tag, change `TAG_TO_MONITOR` in your `.env`. +- **Formatting:** The embed includes a snippet of the question body, with code blocks formatted for Discord. +- **Error Handling:** If the bot fails to send a message or update sent IDs, errors are logged to the console. +- **API Limits:** For higher Stack Exchange API limits, set `STACKEXCHANGE_KEY` in your `.env`. + +## Project Structure + +``` +src/ + config/ # Environment variable validation + services/ # Stack Overflow, Discord, and persistence logic + utils/ # HTML formatting for Discord + index.ts # Entry point +data/ + sentIds.json # Tracks sent question IDs +``` + +## Troubleshooting + +- **No new questions:** If there are no new questions, the bot will log "No new questions." +- **Webhook errors:** Double-check your Discord webhook credentials. +- **Git errors:** Ensure your environment has git installed and configured for auto-commits. + +## License + +MIT + +Feel free to further customize this README for your community or deployment environment! diff --git a/package.json b/package.json index a31dcd1..e1adea9 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,15 @@ { "name": "wdio-discord-bot", + "author": "Rondleysg", + "license": "MIT", + "description": "", "version": "1.0.0", - "main": "index.js", + "private": true, "scripts": { "build": "tsc", "start": "npm run build && node dist/index.js", "dev": "ts-node src/index.ts" }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", "dependencies": { "@octokit/rest": "^21.1.1", "discord.js": "^14.19.3", diff --git a/src/index.ts b/src/index.ts index bcf0932..b4ef438 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,23 +1,20 @@ -import { env } from './config/env'; import { fetchLatestQuestions } from './services/stackoverflow'; import { formatBody } from './utils/htmlFormatter'; import { sendQuestion } from './services/notifier'; import { loadSentIds, saveSentIds } from './services/persistence'; -(async () => { - const sent = await loadSentIds(); - const questions = await fetchLatestQuestions(); - const newQs = questions.filter(q => !sent.has(q.question_id)); +const sent = await loadSentIds(); +const questions = await fetchLatestQuestions(); +const newQs = questions.filter((q) => !sent.has(q.question_id)); - for (const q of newQs.reverse()) { - const snippet = formatBody(q.body).split('\n').slice(0, 10).join('\n').substring(0, 1024); - await sendQuestion(q, snippet); - sent.add(q.question_id); - } +for (const q of newQs.reverse()) { + const snippet = formatBody(q.body).split('\n').slice(0, 10).join('\n').substring(0, 1024); + await sendQuestion(q, snippet); + sent.add(q.question_id); +} - if (newQs.length > 0) { - await saveSentIds(sent); - } else { - console.log('No new questions.'); - } -})(); \ No newline at end of file +if (newQs.length > 0) { + await saveSentIds(sent); +} else { + console.log('No new questions.'); +} \ No newline at end of file diff --git a/src/services/persistence.ts b/src/services/persistence.ts index ece0d93..96c6be7 100644 --- a/src/services/persistence.ts +++ b/src/services/persistence.ts @@ -5,23 +5,31 @@ import path from 'path'; const DATA_FILE = path.resolve(__dirname, '../../data/sentIds.json'); export async function loadSentIds(): Promise> { - try { - const raw = await fs.readFile(DATA_FILE, 'utf-8'); - return new Set(JSON.parse(raw)); - } catch { + const isExisting = await fs.access(DATA_FILE).then(() => true).catch(() => false); + if (!isExisting) { + await fs.writeFile(DATA_FILE, JSON.stringify([], null, 2)); return new Set(); } + + const raw = await fs.readFile(DATA_FILE, 'utf-8'); + return new Set(JSON.parse(raw)); } export async function saveSentIds(ids: Set) { const arr = Array.from(ids); await fs.writeFile(DATA_FILE, JSON.stringify(arr, null, 2)); + // Auto‑commit and push - exec( + return new Promise((resolve, reject) => exec( `git add ${DATA_FILE} && git commit -m "chore: update sent question IDs" && git push`, (err, stdout, stderr) => { - if (err) console.error('Git commit failed:', stderr, stdout); - else console.log('Sent IDs file committed.'); + if (err) { + console.error('Git commit failed:', stderr, stdout); + return reject(err); + } + + console.log('Sent IDs file committed.'); + resolve(); } - ); + )); } \ No newline at end of file diff --git a/src/services/stackoverflow.ts b/src/services/stackoverflow.ts index 1e1d6da..e974637 100644 --- a/src/services/stackoverflow.ts +++ b/src/services/stackoverflow.ts @@ -17,7 +17,10 @@ export async function fetchLatestQuestions(): Promise { pagesize: "10", filter: "withbody", }); - if (env.STACKEXCHANGE_KEY) params.append("key", env.STACKEXCHANGE_KEY); + + if (env.STACKEXCHANGE_KEY) { + params.append("key", env.STACKEXCHANGE_KEY) + }; const url = `https://api.stackexchange.com/2.3/questions?${params}`; const res = await fetch(url); diff --git a/tsconfig.json b/tsconfig.json index f0c773e..a0868c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { "compilerOptions": { - "target": "ES2020", - "module": "commonjs", + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "outDir": "dist", "rootDir": "src", - "resolveJsonModule": true, "skipLibCheck": true } } From fbcf87f190b27dc37bbb34620afe50ae7c14038a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Sat, 10 May 2025 15:58:44 -0700 Subject: [PATCH 2/2] use 'node:...' for Node.js imports --- src/services/notifier.ts | 3 ++- src/services/persistence.ts | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/services/notifier.ts b/src/services/notifier.ts index c1f60ad..717e1f9 100644 --- a/src/services/notifier.ts +++ b/src/services/notifier.ts @@ -1,6 +1,7 @@ import { WebhookClient, EmbedBuilder } from "discord.js"; -import { env } from "../config/env"; + import { Question } from "./stackoverflow"; +import { env } from "../config/env"; const webhook = new WebhookClient({ id: env.DISCORD_WEBHOOK_ID, diff --git a/src/services/persistence.ts b/src/services/persistence.ts index 96c6be7..f2fdfd7 100644 --- a/src/services/persistence.ts +++ b/src/services/persistence.ts @@ -1,6 +1,6 @@ -import fs from 'fs/promises'; -import { exec } from 'child_process'; -import path from 'path'; +import fs from 'node:fs/promises'; +import cp from 'node:child_process'; +import path from 'node:path'; const DATA_FILE = path.resolve(__dirname, '../../data/sentIds.json'); @@ -20,7 +20,7 @@ export async function saveSentIds(ids: Set) { await fs.writeFile(DATA_FILE, JSON.stringify(arr, null, 2)); // Auto‑commit and push - return new Promise((resolve, reject) => exec( + return new Promise((resolve, reject) => cp.exec( `git add ${DATA_FILE} && git commit -m "chore: update sent question IDs" && git push`, (err, stdout, stderr) => { if (err) {