npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jsdiscordbot

v1.0.9

Published

A Telegraf-inspired Discord.js bot framework with scenes, session, and markup.

Readme

A Telegraf-inspired framework for building Discord bots with modern scene and session management.

💖 Support My Work

If you enjoy my projects and want to support me, you can donate using crypto 🚀


🌍 Wallet Addresses

| Coin | Network | Address | |------|---------|---------| | 🟠 Bitcoin (BTC) | Bitcoin | 1PouA4hPvM4sgg26RbbevBuG3JVF2yGiTt | | 🔵 Ethereum (ETH) | ERC20 | 0x35e743344347b02e0071e8ec2a9850ec4fc879f2 | | 🟢 Tether (USDT) | TRC20 | TWUbLudtqA65r6tFXrhBigVxgH22GikMct | | 🟢 Tether (USDT) | ERC20 | 0x35e743344347b02e0071e8ec2a9850ec4fc879f2 |


Donate Bitcoin Donate Ethereum Donate USDT


✨ Thank you for your support — it really means a lot! 🙏

Features

  • Step-by-step scene system (Wizard-like)
  • In-memory and file-based session storage
  • Middleware-based architecture
  • Simple API for commands, actions, and media
  • Persistent sessions (optional)
  • Clean, modern codebase
  • Discord.js v14+ compatible

Installation

npm install jsdiscordbot

API

DiscordBot

  • command(cmd, fn) — Register a command handler (exact, regex, or array)
  • action(payload, fn) — Register a button/action handler (exact, regex, or array)
  • hears(pattern, fn) — Register a message handler (exact, regex, or array)
  • use(middleware) — Add middleware
  • launch() — Start the bot

Scene System

  • Scene(name, steps[]) — Create a scene
  • SceneManager() — Manage and register scenes
  • scenes.register(scene) — Register a scene
  • scenes.middleware() — Scene middleware
  • scenes.enter('sceneName') — Enter a scene

Session Middleware

  • session({ type: 'file' }) — Use file-based session (default is in-memory)

Markup (Buttons, Keyboards, Media)

Import Markup:

import { Markup } from 'jsdiscordbot'

Buttons/Keyboard

await ctx.reply(
  'Choose an option:',
  Markup.keyboard([
    [
        Markup.button({ label: 'Yes', type: "button", idOrUrl: 'ID', style: 'SUCCESS' }),
        Markup.button('Label', 'button', 'ID', 'PRIMARY')
        Markup.button('Label', 'url', 'https://example.com')
    ],
  ])
)

Media Replies

await ctx.replyWithPhoto('https://example.com/photo.jpg', 'Photo!', Markup.keyboard(...))
await ctx.replyWithDocument('https://example.com/file.pdf', 'Document!', Markup.keyboard(...))

Handling Actions

bot.action('YES', (ctx) => ctx.reply('You clicked Yes!'))
bot.action(/LIKE|DISLIKE/, (ctx) => ctx.reply('You clicked Like or Dislike!'))
bot.action(['A', 'B'], (ctx) => ctx.reply('You clicked A or B!'))

Chat Invite Link

Create a Discord chat invite link for the current channel (like TelegrafJS):

// Create an invite link for up to 10 users (link expires after 10 uses)
const inviteUrl = await ctx.chatInviteLink({ maxUses: 10 })
await ctx.reply(`Invite link: ${inviteUrl}`)

Options:

  • maxUses: Maximum number of users who can join with the link (default: 1)
  • maxAge: Link expiration time in seconds (default: 3600)
  • temporary: Grant temporary membership (default: false)
  • unique: Create a unique invite (default: true)
  • reason: Reason for invite creation

After the specified number of users join, the link becomes invalid automatically.

Example: Registration Scene

const registrationScene = new Scene('registration', [
  async (ctx) => {
    await ctx.reply('Welcome to registration! What is your first name?')
  },
  async (ctx) => {
    if (!ctx.text) {
      await ctx.reply('Please enter your first name.')
      return false
    }
    ctx.session.firstName = ctx.text
    await ctx.reply('What is your last name?')
  },
  async (ctx) => {
    if (!ctx.text) {
      await ctx.reply('Please enter your last name.')
      return false
    }
    ctx.session.lastName = ctx.text
    await ctx.reply('What is your email address?')
  },
  async (ctx) => {
    if (!ctx.text || !/\S+@\S+\.\S+/.test(ctx.text)) {
      await ctx.reply('Please enter a valid email address.')
      return false
    }
    ctx.session.email = ctx.text
    await ctx.reply(
      `Registration complete!\nFirst Name: ${ctx.session.firstName}\nLast Name: ${ctx.session.lastName}\nEmail: ${ctx.session.email}`
    )
    await registrationScene.leave(ctx)
  },
])

const scenes = new SceneManager()
scenes.register(registrationScene)
bot.use(session())
bot.use(scenes.middleware())
bot.command('/register', async (ctx) => {
  await registrationScene.enter(ctx)
})

Event System

Handle all incoming messages and button actions:

bot.on('message', async (ctx) => {
  console.log('Received message:', ctx.text)
})

bot.on('action', async (ctx) => {
  console.log('Button clicked:', ctx.payload)
})

Example: Simple Bot

import 'dotenv/config'
import DiscordBot, { Markup, Scene, SceneManager, session } from 'jsdiscordbot'

const testPhotoUrl = 'https://www.w3schools.com/w3images/lights.jpg'
const testDocUrl =
  'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
const testAudioUrl =
  'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'
const testVideoUrl = 'https://www.w3schools.com/html/mov_bbb.mp4'

const bot = new DiscordBot({
  token: process.env.DISCORD_BOT_TOKEN,
})

bot.catch(async (err, ctx) => {
  console.error('Global error:', err)
  if (ctx && ctx.reply) {
    await ctx.reply('An error occurred: ' + err.message)
  }
})

const registrationScene = new Scene('registration', [
  async (ctx) => {
    await ctx.reply('Welcome to registration! What is your first name?')
  },
  async (ctx) => {
    if (!ctx.text) return false
    ctx.session.firstName = ctx.text
    await ctx.reply('What is your last name?')
  },
  async (ctx) => {
    if (!ctx.text) {
      await ctx.reply('Please enter your last name.')
      return false
    }
    ctx.session.lastName = ctx.text
    await ctx.reply('What is your email address?')
  },
  async (ctx) => {
    if (!ctx.text || !/\S+@\S+\.\S+/.test(ctx.text)) {
      await ctx.reply('Please enter a valid email address.')
      return false
    }
    ctx.session.email = ctx.text
    await ctx.reply(
      `Registration complete!\nFirst Name: ${ctx.session.firstName}\nLast Name: ${ctx.session.lastName}\nEmail: ${ctx.session.email}`
    )
    await registrationScene.leave(ctx)
  },
])

const sceneManager = new SceneManager()
sceneManager.register(registrationScene)
bot.use(session())
bot.use(sceneManager.middleware())

bot.hears(['register', '/register'], async (ctx) => {
  await registrationScene.enter(ctx)
})

bot.command('/photo', async (ctx) => {
  await ctx.replyWithPhoto(
    testPhotoUrl,
    'Here is a photo with buttons!',
    Markup.keyboard([
      [
        Markup.button('Like', 'button', 'LIKE', 'PRIMARY'),
        Markup.button('Dislike', 'button', 'DISLIKE', 'DANGER'),
      ],
    ])
  )
})

bot.command('/document', async (ctx) => {
  await ctx.replyWithDocument(
    testDocUrl,
    'dummy.pdf',
    'Here is a document with buttons!',
    Markup.keyboard([[Markup.button('Download', 'DOWNLOAD', 'SUCCESS')]])
  )
})

bot.command('/pdf', async (ctx) => {
  await ctx.replyWithPDF(
    testDocUrl,
    'dummy.pdf',
    'Here is a PDF with buttons!',
    Markup.keyboard([[Markup.button('Open PDF', 'button', 'OPEN_PDF', 'PRIMARY')]])
  )
})

bot.command('/audio', async (ctx) => {
  await ctx.replyWithDocument(
    testAudioUrl,
    'test-audio.mp3',
    'Here is an audio file with buttons!',
    Markup.keyboard([[Markup.button('Play', 'button', 'PLAY_AUDIO', 'SUCCESS')]])
  )
})

bot.command('/video', async (ctx) => {
  await ctx.replyWithDocument(
    testVideoUrl,
    'test-video.mp4',
    'Here is a video file with buttons!',
    Markup.keyboard([[Markup.button('Play', 'button', 'PLAY_VIDEO', 'PRIMARY')]])
  )
})

bot.command('/kick', async (ctx) => {
  const args = ctx.text?.split(' ')
  const userId = args && args[1]
  if (!userId) {
    await ctx.reply('Usage: /kick <userId>')
    return
  }
  const success = await ctx.kickMember(userId, 'Kicked by bot command')
  if (success) {
    await ctx.reply(`User ${userId} was kicked.`)
  } else {
    await ctx.reply(`Failed to kick user ${userId}.`)
  }
})

bot.command('/ban', async (ctx) => {
  const args = ctx.text?.split(' ')
  const userId = args && args[1]
  if (!userId) {
    await ctx.reply('Usage: /ban <userId>')
    return
  }
  const success = await ctx.banMember(userId, 'Banned by bot command')
  if (success) {
    await ctx.reply(`User ${userId} was banned.`)
  } else {
    await ctx.reply(`Failed to ban user ${userId}.`)
  }
})

bot.hears(['hello', 'hi'], async (ctx) => {
  await ctx.reply('Hello! How can I assist you today?')
})

bot.command('/keyboard', async (ctx) => {
  await ctx.reply(
    'Choose an option:',
    Markup.keyboard([
      [
        Markup.button('Yes', 'button', 'YES', 'PRIMARY'),
        Markup.button('No', 'button', 'NO', 'DANGER'),
      ],
    ])
  )
})

bot.action('YES', async (ctx) => {
  if (ctx.event && typeof ctx.event.reply === 'function') {
    await ctx.event.reply({ content: 'You clicked Yes!', ephemeral: true })
  } else {
    await ctx.reply('You clicked Yes!')
  }
  await ctx.deleteMessage()
})

bot.action('NO', async (ctx) => {
  if (ctx.event && typeof ctx.event.reply === 'function') {
    await ctx.event.reply({ content: 'You clicked No!', ephemeral: true })
  } else {
    await ctx.reply('You clicked No!')
  }
  await ctx.deleteMessage()
})

bot.on('new_member', async (ctx) => {
  await ctx.reply(`Welcome <@${ctx.event.id}> to the server!`)
})

bot.on('remove_member', async (ctx) => {
  await ctx.reply(`Goodbye <@${ctx.event.id}>!`)
})

bot.on('message_reaction_add', async (ctx) => {
  await ctx.reply(`Reaction added by <@${ctx.event.id}>!`)
})

bot.on('message_reaction_remove', async (ctx) => {
  await ctx.reply(`Reaction removed by <@${ctx.event.id}>!`)
})

// --- Exact string match ---
bot.hears('ping', async (ctx) => {
  await ctx.reply('pong (exact string match)')
})

bot.command('echo', async (ctx) => {
  await ctx.reply('Echo command triggered (exact string match)')
})

bot.action('EXACT_ACTION', async (ctx) => {
  if (ctx.event && typeof ctx.event.reply === 'function') {
    await ctx.event.reply({ content: 'Exact action matched!', ephemeral: true })
  } else {
    await ctx.reply('Exact action matched!')
  }
})

// --- Regex match ---
bot.hears(/\d{4}/, async (ctx) => {
  await ctx.reply('Matched a 4-digit number (regex)')
})

bot.command(/test.*/, async (ctx) => {
  await ctx.reply('Test command triggered (regex)')
})

bot.action(/REGEX_.*/, async (ctx) => {
  if (ctx.event && typeof ctx.event.reply === 'function') {
    await ctx.event.reply({ content: 'Regex action matched!', ephemeral: true })
  } else {
    await ctx.reply('Regex action matched!')
  }
})

// --- Array match ---
bot.hears(['foo', 'bar'], async (ctx) => {
  await ctx.reply('Matched foo or bar (array)')
})

bot.command(['multi1', 'multi2'], async (ctx) => {
  await ctx.reply('Multi command triggered (array)')
})

bot.action(['ACTION1', 'ACTION2'], async (ctx) => {
  if (ctx.event && typeof ctx.event.reply === 'function') {
    await ctx.event.reply({ content: 'Array action matched!', ephemeral: true })
  } else {
    await ctx.reply('Array action matched!')
  }
})

bot.on('message', async (ctx) => {
  console.log(`Received message: ${ctx.text}`)
})

bot.launch()

Custom Express Hosting

You can host DiscordBot with your own Express app :

import express from 'express'
import DiscordBot from 'jsdiscordbot'

const bot = new DiscordBot({
  /* ...config... */
})

const app = express()
app.use(express.json())

// Mount Messenger webhook route
app.use('/webhook', bot.app)

// Start your own server
app.listen(3000, () => {
  console.log('Custom Express server running on port 3000')
})

Do not use bot.launch() if you want full control over your Express server.

License

MIT

Author

KH Rasedul — [email protected]